Dareplane Python Utils
This module includes utilities for python which are used within the dareplane framework. It contains functionality which shared can be reused within multiple modules. This currently includes:
- A
DefaultServer- which will be loaded an extended within each module to implement the dareplane API logging- which contains the standard formatting and a SocketHandler which is modified to sendjsonrepresentations of the logging records to the default logging server port (9020). This is used to enable cross process logging.- A
StreamWatcherimplementation - which is a utility class to query a single LSL stream into a ring buffer. - A
ModuleConnection- which, together with alauncherandcommunicator, is used to launch and interact with other modules (currently supports launching Python and .exe programs).
Default Dareplane Server
This default server is used by all Dareplane python modules as a starting
point for their TCP socket. The idea is to have a single source for common
functionality and patch everything that is model specific on top of this
Functional incarnations
Currently we are faced with two functional incarnations of servers
- Spawning functionality from the server in a separate thread, being linked via events to the main thread (usually the server).
- Spawning a subprocess for running functionality - Currently necessary for running
psychopyas it cannot be run from outside the main thread.
The TCP command protocol
Framing
Every command must be terminated by a ;. TCP is a byte stream without message
boundaries, so a single recv can return half a command or several concatenated ones. The
server therefore buffers incoming bytes and only dispatches a command once it sees the
delimiter. A command without a trailing ; stays in the buffer and is never executed - it
waits for the rest of the message to arrive.
sock.sendall(b"MYCOMMAND;") # dispatched
sock.sendall(b"CMD_A;CMD_B;CMD_C;") # all three dispatched, in order
sock.sendall(b"MYCOMMAND") # buffered, nothing happens
Empty segments and pure whitespace between delimiters are ignored, so ;;CMD; is fine.
If you connect via ModuleConnection, this is handled for you - send_message appends the
delimiter when it is missing:
conn.send_message(b"UP") # b"UP;" goes on the wire
Command syntax
A command consists of a primary command - a PCOMM - optionally followed by a single
|-separated JSON object of keyword arguments. PCOMMS are the names registered in the
pcommand_map, and only these two forms are valid:
| sent | resulting call |
|---|---|
PCOMM; |
func() |
PCOMM|{"a": 1}; |
func(a=1) |
There are no positional arguments - everything a handler needs is passed as keyword arguments
through the JSON object. An empty payload (PCOMM|; or PCOMM|{};) is the same as sending the
bare PCOMM;.
Anything else is logged as an error and dropped: a payload that is not valid JSON
(PCOMM|abc;), a payload that is not a JSON object (PCOMM|[1, 2];) and any message with more
than one | (PCOMM|a|{};).
Built-in PCOMMS
These are handled by every server before the module specific pcommand_map is consulted:
| PCOMM | effect |
|---|---|
STOP; |
stop all threads and subprocesses spawned by this module |
CLOSE; |
stop listening and shut the server down |
UP; |
health check, replies with 1 |
GET_PCOMMS; |
replies with a |-separated list of available PCOMMS, including STOP and CLOSE |
Any other PCOMM is looked up in pcommand_map; unknown PCOMMS are logged as a warning
and otherwise ignored.
Defaults
DefaultServer is a dataclass - all of the below are constructor arguments:
| field | default | meaning |
|---|---|---|
port |
8080 |
port the server binds to |
ip |
"0.0.0.0" |
interface the server binds to |
nlisten |
10 |
backlog of queued connections |
name |
"default_server" |
used in the connection banner Connected to <name> |
delimiter |
b";" |
PCOMM terminator used for framing |
msg_interpreter |
interpret_msg |
maps a parsed PCOMM onto a pcommand_map entry |
thread_stopper |
stop_thread |
how spawned threads are joined on STOP/shutdown |
proc_stopper |
stop_process |
how spawned subprocesses are terminated |
pcommand_map |
{} |
the module specific PCOMM -> callable mapping |
The default logging server port is 9020 (see the Logging section below).
Handlers registered in pcommand_map must return one of:
- an
int- fire and forget, nothing is tracked, - a
tuple[threading.Thread, threading.Event]- the server tracks the thread and sets the event onSTOP, - a
subprocess.Popen- the server tracks and terminates the process onSTOP.
Anything else raises UnknownMsgInterpretation.
from dareplane_utils.default_server.server import DefaultServer
server = DefaultServer(port=8080, name="my_module")
server.pcommand_map = {"MYCOMMAND": my_handler}
server.init_server()
server.start_listening()
Logging
The logging tools allow two main entry point, which are from dareplane_utils.logging.logger import get_logger, which is used to get a logger with the default configuration and from dareplane_utils.logging.server import LogRecordSocketReceiver which is used to spawn up a server for consolidating logs of different processes.
StreamWatcher
StreamWatcher are a convenient utility around LSL stream inlets. They are basically a ring buffer for reading data to a numpy array. StreamWatchers are:
- initialized with a target stream name and a buffer size in seconds specified by
buffer_size - connected to the target LSL stream
- updated to fetch the latest data (usually done in a loop)
initialize a StreamWatcher
from dareplane_utils.stream_watcher.lsl_stream_watcher import StreamWatcher
STREAM_NAME = "my_stream"
BUFFER_SIZE_S = 5 # the required buffer size will be calculated from the LSL
# streams meta data
sw = StreamWatcher(
STREAM_NAME,
buffer_size_s=BUFFER_SIZE_S,
)
connect to the stream
# Either use the self.name or a provided identifier dict to hook up to an LSL stream
sw.connect_to_stream()
update
sw.update()
Update will call the following method:
def update(self):
"""Look for new data and update the buffer"""
samples, times = self.inlet.pull_chunk()
self.add_samples(samples, times)
self.samples = samples
self.n_new += len(samples)
Getting data
To get the data from the StreamWatcher you can either grab the full ring buffer from the instance attributes
sw.buffer # ring buffer for data
sw.buffer_t # ring buffer for time stamps
sw.curr_i # current position of the head in the ring buffer
or you usually want the more convenient way by using the unfold_buffer method,
which returns a chronologically sorted array ([-1] is the most recent data
point and [0] is the oldest data point).
sw.unfold_buffer() # sorted data
sw.unfold_buffer_t() # sorted time stamps
## The above is using the following implementation
def unfold_buffer(self):
return np.vstack(
[self.buffer[self.curr_i :], self.buffer[: self.curr_i]]
)
Event Loop
A class that implements a custom event loop with precise timing.
The EventLoop uses dareplane_utils.general.time.sleep_s for more precise sleep timing at the expense of CPU usage.
Callbacks are the means of interacting with the event loop. There are two types of callbacks:
- Periodic callbacks: These are executed at regular intervals.
- One-time callbacks: These are executed once and then removed from the list of callbacks. One-time callback can furthermore be scheduled to run at a specific time in the future.
Callbacks can be any callable function, which gets one and only one argument, which is a context object, that can be of type any. This ensures that any type of input can be implemented.
def no_arg_callback():
print("Running with no args")
evloop = EventLoop(dt_s=0.1) # process callbacks every 100ms
# for a callback with no args we use lambda to blank the callback arg
evloop.add_callback_once(lambda ctx: no_arg_callback())
Building the documentation
The API reference is generated from the docstrings with quartodoc and rendered by quarto (which has to be installed separately).
uv pip install -e ".[docs]"
python -m quartodoc build # regenerate reference/ and _sidebar.yml from _quarto.yml
quarto render # render the site into _site/
reference/, objects.json and _site/ are generated output and are gitignored -
_sidebar.yml is the only build product that is tracked, so re-run quartodoc build
whenever sections are added to _quarto.yml.
Note that quartodoc cannot render the numpy Methods docstring section - it generates a
methods table from the class members itself, so list methods in a Notes section instead if
they need extra explanation.
TODO
- channel names are only initialized on connection
Metadata
Release files for dareplane-utils 0.0.24
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| dareplane_utils-0.0.24.tar.gz | 56.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dareplane_utils-0.0.24-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 93.6 kB
Release files / dareplane_utils-0.0.24.tar.gz
| Download URL | dareplane_utils-0.0.24.tar.gz |
|---|---|
| Size | 56.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
68ffaa300fdc7f62e8eb169755024c88730b23ab87fc527373ff72fd4925b2a2
|
|
BLAKE2b-256 checksum How to use checksums |
99d530c0fb340c080d162370b0b59204391cae0b7b96287b5423de7ab8596c16
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 12, 2026.
Transparency logRelease files / dareplane_utils-0.0.24-py3-none-any.whl
| Download URL | dareplane_utils-0.0.24-py3-none-any.whl |
|---|---|
| Size | 37.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0926aded7d8ca2130790718bbb0d4d65e1e8c8c9f5a5bc8979bd67c14a725b8b
|
|
BLAKE2b-256 checksum How to use checksums |
07bdf1825c9341b09d1d202f9d95cd4f93d0529686e22448b4e65bc58133cf89
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 12, 2026.
Transparency log