devices#

class mesofield.devices.BaseDevice[source]#

Bases: object

Default lifecycle skeleton for non-Qt hardware devices.

Constructor accepts an optional cfg mapping (the YAML stanza for this device). Common keys are auto-extracted:

  • id / device_id -> self.device_id

  • primary: true -> self.is_primary

A logger is created automatically as f"{module}.{class}[{device_id}]".

__init__(cfg=None, **kwargs)[source]#
Parameters:
Return type:

None

arm(config)[source]#

Per-run preparation. No-op by default.

Parameters:

config (ExperimentConfig)

Return type:

None

property calibration: Dict[str, Any]#

Device-specific constants worth recording with the data.

Default: everything in cfg that isn’t an orchestration key. Override on a subclass to curate the list explicitly.

sidecars()[source]#

Auxiliary files this device writes alongside its primary output.

Default: none. Override to declare extra sidecars (masks, regions, derived parameter files) so they ride in the manifest with a role and schema_version instead of being discovered by glob.

The camera classes’ per-frame metadata JSON is the primary sidecar and lives on self.metadata_path – not here. Use this method for the extra files only.

Returns a list of mesokit_schema.SidecarEntry-shaped mappings or instances. The Procedure relativises any absolute paths.

Return type:

list

class mesofield.devices.BaseDataProducer[source]#

Bases: BaseDevice

Base class for devices that stream samples to the DataQueue.

Subclasses produce data by calling record(), which timestamps, appends to an in-memory buffer, and emits signals.data(payload, ts) in one step.

The default save_data() writes the buffer as a two-column CSV (timestamp,payload). Override for binary or domain-specific formats.

__init__(cfg=None, **kwargs)[source]#
Parameters:
Return type:

None

record(payload, ts=None)[source]#

Buffer payload and emit signals.data.

Returns the timestamp used.

Parameters:
Return type:

float

queue_payload(payload)[source]#

Filter/reshape what this producer contributes to the dataqueue.

signals.data is the full-fidelity stream that feeds this device’s own buffer (save_data -> CSV) and any live plots. The session-wide DataQueue is a cross-device alignment/event timeline, and a producer may want to contribute only a subset (or a reshaped form) of its samples to it.

DataManager calls this for every sample before pushing it onto the queue. Return:

  • the (possibly transformed) payload to push, or

  • None to drop this sample from the queue entirely.

Default: push the payload unchanged. Override on a subclass to e.g. push only discrete events, or strip a high-rate channel from the queue while leaving the CSV and plots untouched.

Parameters:

payload (Any)

Return type:

Any | None

arm(config)[source]#

Default arm: clear buffer and resolve output_path.

config is expected to expose make_path(name, ext, bids) (see mesofield.config.ExperimentConfig).

Parameters:

config (ExperimentConfig)

Return type:

None

class mesofield.devices.BaseSerialDevice[source]#

Bases: BaseDataProducer

Polling device for line-based serial protocols (Arduino/Teensy/etc.).

Subclasses override parse_line(). Optionally override setup_serial() for post-open initialisation (handshakes, buffer drain, configuring device-side parameters).

Configuration keys read from cfg:

  • port (str, required when development_mode=False)

  • baudrate (int, default 115200)

  • timeout (float, default 0.1) — pyserial readline timeout.

  • dtr (bool | None, default None) — set to False to suppress Arduino auto-reset on connect. None keeps the OS default.

  • connect_delay (float, default 0.0) — seconds to wait after opening the port before reads begin; common Arduinos need ~2.0. The input buffer is flushed after the delay.

  • development_mode (bool, default False) — skip opening the port so the GUI / Procedure can launch without hardware. send_line becomes a no-op; the polling thread idles.

__init__(cfg=None, **kwargs)[source]#
Parameters:
Return type:

None

parse_line(line)[source]#

Decode one raw serial line into (payload, ts) or None.

Parameters:

line (bytes)

Return type:

Tuple[Any, float | None] | None

setup_serial()[source]#

Hook called once after the port is opened. Default no-op.

Override to send a handshake, query firmware version, configure device-side parameters, etc.

Return type:

None

send_line(payload, *, newline=b'\n')[source]#

Write a command to the device. Thread-safe with the reader.

Accepts str (UTF-8 encoded) or bytes. newline is appended unless payload already ends with it. In development_mode the bytes are logged and discarded. Returns the bytes that were written (or would have been).

Parameters:
Return type:

bytes

class mesofield.devices.Nidaq[source]#

Bases: BaseDataProducer

External start-trigger + TTL edge counter on an NI-DAQ board.

__init__(cfg=None, **kwargs)[source]#
Parameters:
Return type:

None

reset()[source]#

Stop the worker (if running) and reset the NI-DAQ device.

Return type:

None

test_connection()[source]#

Pulse the DO line high ~3 s as a wiring check (logs, never raises).

Return type:

None

class mesofield.devices.MMCamera[source]#

Bases: BaseCamera, DataProducer, HardwareDevice

Micro-Manager-backed camera.

Inherits the common camera surface (identity, output paths, manifest metadata, arm / set_sequence defaults, status, calibration) from BaseCamera, and duck-types the DataProducer / HardwareDevice Protocols so existing isinstance() checks keep working. The actual frame flow is driven by pymmcore-plus’s MDA event system; this class wires those events into the standard DeviceSignals bundle and constructs an OMEWriter (OME-TIFF) or CV2Writer (MP4) for the output.

__init__(cfg)[source]#
Parameters:

cfg (dict)

set_sequence(build_mda)[source]#

Build the MDA sequence (Micro-Manager backend only).

Parameters:

build_mda (Callable[[DataProducer], Any])

initialize()[source]#

Apply the YAML properties block to the underlying camera.

Each {device_id: {property: value}} pair is forwarded to the backend (core.setROI for ROIs, core.setProperty otherwise) with special handling for the synthetic fps, viewer_type, and auto_contrast keys.

start_led_sequence(pattern)[source]#

Start the LED pattern.

If led_serial is configured on this camera, sends the configured raw byte sequences via MM’s SerialManager. Otherwise falls back to the original Arduino-Switch.State.loadSequence/startSequence path.

Return type:

None

stop_led_sequence()[source]#

Stop the LED pattern (mirror of start_led_sequence()).

Return type:

None

start()[source]#

Launch the MDA sequence non-blocking.

Returns:

Always True. The sequence runs asynchronously on the camera backend; lifecycle is reported via self.signals.

Return type:

bool

stop()[source]#

Stop acquisition for any running Micro-Manager camera (incl. primary).

stopSequenceAcquisition is non-joining and idempotent: on natural completion the engine already stopped the sequence (no-op here), and on abort/duration-cap it flips the flag the engine loop polls so the run ends and buffered frames flush. Safe from any thread — unlike mda.cancel(), which joins the runner and would self-deadlock when cleanup runs on the MDA worker thread (the primary’s finished path).

Return type:

bool

get_data()[source]#

Return the latest captured frame, or None if not active.

snap()[source]#

Capture a single frame via mmcore.snap() and save a snapshot PNG.

start_live()[source]#

Begin continuous (untimed) sequence acquisition for preview.

Return type:

None

stop_live()[source]#

End the continuous sequence acquisition started by start_live.

Return type:

None

info()[source]#

Return the verbose multi-line description (module path, MRO, properties).

Return type:

str

class mesofield.devices.OpenCVCamera[source]#

Bases: BaseCamera, QThread

Background-thread OpenCV camera capturing to MP4.

Emits via self.signals (a mesofield.signals.DeviceSignals):
  • signals.started / signals.finished for lifecycle.

  • signals.data(idx, device_ts) per frame, consumed by DataManager.register_hardware_device().

Plus Qt live-preview signals (GUI-only, decoupled from DataQueue):
  • frame_ready(np.ndarray) / image_ready(np.ndarray).

Inherits the common camera surface (identity, output paths, manifest metadata, arm/set_sequence defaults) from BaseCamera, and runs its own capture loop on top of QThread.

__init__(cfg)[source]#
Parameters:

cfg (Dict[str, Any])

initialize()[source]#

Verify the camera opens and actually delivers a frame.

isOpened() is not proof on Windows: it returns True while read() fails forever (wrong backend / capture format), which manifests as a camera that “initializes” but shows no live view. So we read a frame here and fail loudly if none arrives.

Return type:

bool

set_writer(make_path)[source]#

Resolve the output path and build the CV2Writer.

BaseCamera.set_writer resolves output_path, constructs the CV2Writer (the project’s shared MP4 writer), and copies its sidecar path onto metadata_path. The capture loop drives the writer directly via begin/add_frame/finish.

Parameters:

make_path (Callable[[str, str, str, bool], str])

Return type:

None

start()[source]#

Spawn the capture thread and begin writing frames to MP4.

Returns:

True if the thread started, False if it was already running.

Return type:

bool

stop()[source]#

Signal the capture thread to stop and wait for it to join.

Return type:

bool

snap()[source]#

Open the camera, read one frame, return it without recording.

Return type:

ndarray

start_live()[source]#

Start the capture thread WITHOUT a writer (preview-only).

Return type:

None

stop_live()[source]#

End preview capture started by start_live.

Return type:

None

get_data()[source]#

Return the most recent frame index/timestamp pair, or None.

Return type:

Dict[str, Any] | None

save_data(path=None)[source]#

No-op: the MP4 and its _frame_metadata.json sidecar are written by the CV2Writer when the capture loop finishes.

Parameters:

path (str | None)

Return type:

None

run(self)[source]#
Return type:

None

class mesofield.devices.SerialWorker[source]#

Bases: BaseSerialDevice

Arduino wheel-encoder device.

__init__(cfg=None, serial_port=None, baud_rate=None, sample_interval=None, wheel_diameter=None, cpr=None, development_mode=None, **kwargs)[source]#
Parameters:
  • cfg (Dict[str, Any] | None)

  • serial_port (str | None)

  • baud_rate (int | None)

  • sample_interval (int | None)

  • wheel_diameter (float | None)

  • cpr (int | None)

  • development_mode (bool | None)

  • kwargs (Any)

Return type:

None

parse_line(line)[source]#

Decode one raw serial line into (payload, ts) or None.

Parameters:

line (bytes)

Return type:

Tuple[int, float | None] | None

Parser#

alias of WheelEncoder

class mesofield.devices.EncoderSerialInterface[source]#

Bases: BaseSerialDevice

Teensy encoder/treadmill device.

Constructor accepts either a cfg dict (BaseSerialDevice-style) or legacy positional/keyword args (port, baudrate) for backward compatibility with mesofield.hardware.

__init__(cfg=None, port=None, baudrate=None, **kwargs)[source]#
Parameters:
Return type:

None

parse_line(line)[source]#

Decode one raw serial line into (payload, ts) or None.

Parameters:

line (bytes)

Return type:

Tuple[Dict[str, Any], float | None] | None

send_command(command)[source]#

Send a single-character firmware command (no newline).

Parameters:

command (str)

Return type:

bytes

Parser#

alias of TreadmillSource

class mesofield.devices.PsychoPyDevice[source]#

Bases: SubprocessStimulusDevice

Stimulus device that launches a PsychoPy script as a subprocess.

__init__(cfg)[source]#
Parameters:

cfg (Dict[str, Any])

serves_task(task, config)[source]#

Serve a task iff the task->script map has an entry for it.

With no map (legacy single-script experiments) PsychoPy serves every task, preserving the old behavior.

Return type:

bool

prepare(config)[source]#

Resolve the script path and serialize parameters for the subprocess.

Parameters are sent as base64-encoded JSON (a single safe argv token) so the PsychoPy interpreter decodes them with only the stdlib – the script rebuilds an attribute namespace, e.g. config = types.SimpleNamespace(**json.loads(base64.b64decode(sys.argv[1]))).

Return type:

None

preflight()[source]#

Return an actionable error string to abort launch, or None to proceed.

Return type:

str | None

build_command()[source]#

Return the full argv used to launch the stimulus subprocess.

Return type:

List[str]

present_launching()[source]#

Show a non-blocking ‘waiting for PSYCHOPY_READY’ indicator.

Return type:

None

dismiss_launching()[source]#

Dismiss the indicator shown by present_launching(). No-op default.

Return type:

None

present_failure(message, detail='')[source]#

Surface a launch/handshake failure to the operator.

detail carries the child’s last output (see SubprocessSupervisor.output_tail). Logs by default; a GUI subclass shows a dialog.

Parameters:
Return type:

None

confirm_ready_to_record()[source]#

Focused ‘PsychoPy ready – press to start recording’ gate.

Forced to the foreground over PsychoPy’s full-screen window so a spacebar press lands on this dialog (OK is the default button), not on PsychoPy. Dismissing it returns control to the Procedure, which then starts the recording devices; the operator then presses spacebar in the PsychoPy window to begin the stimulus (cameras lead; timelines are aligned post-hoc). Returns False (Cancel) to abort the run.

Return type:

bool

Parser#

alias of Psychopy

class mesofield.devices.MousePortalDevice[source]#

Bases: SubprocessStimulusDevice

Stimulus device that launches MousePortal and feeds it treadmill velocity.

__init__(cfg)[source]#
Parameters:

cfg (Dict[str, Any])

serves_task(task, config)[source]#

Serve a task iff it matches the MousePortal config block’s task.

Each MousePortal configuration corresponds to a single task ID (a list is honored for the future multi-config case). With no task bound, MousePortal serves every task, preserving the single-stimulus behavior.

Return type:

bool

prepare(config)[source]#

Generate the MousePortal cfg.json and wire treadmill forwarding.

The output CSV path is the ExperimentConfig-authoritative path: DataManager.setup assigns self.output_path from DataPaths before arm. We fall back to config.make_path only for standalone use (no DataManager). MousePortal is handed this exact file path and writes only there – it never constructs a BIDS directory layout.

Return type:

None

build_command()[source]#

Return the full argv used to launch the stimulus subprocess.

Return type:

List[str]

launch_env()[source]#

Environment for the MousePortal subprocess.

Ensures Panda3D can find its Config.prc (which carries load-display pandagl). conda-forge’s panda3d on Windows installs the config under <env>\Library\etc\panda3d and the GL plugin under <env>\Library\bin, but Panda3D’s runtime auto-locator does not look there – so no display module loads and ShowBase aborts with “No graphics pipe is available!”. Setting PANDA_PRC_DIR points it at the right directory. Respects an existing PANDA_PRC_DIR and is a no-op when the directory cannot be found (e.g. macOS/pip layouts that already auto-locate correctly).

Return type:

Dict[str, str] | None

preflight()[source]#

Return an actionable error string if MousePortal cannot launch.

Return type:

str | None

on_stop()[source]#

Teardown for anything opened in prepare(). No-op by default.

Return type:

None

expected_experiment_duration()[source]#

Estimate the MousePortal experiment length in seconds.

Mirrors MousePortal’s per-trial resolution (condition override → global) and sums each trial’s duration plus the inter-trial interval that follows it. DURATION-ended trials are exact; DISTANCE/MANUAL trials are non-deterministic, so their per-trial trial_duration (or the global default) is used as an estimate – pair this coupling with duration-based trials for a precise camera preallocation.

Return type:

float

property calibration: Dict[str, Any]#

Record the corridor/experiment parameters with the session.

class mesofield.devices.MockEncoderDevice[source]#

Bases: BaseDataProducer

Synthetic encoder that records random click counts.

__init__(cfg=None, **kwargs)[source]#
Parameters:
Return type:

None

class mesofield.devices.MockFrameProducer[source]#

Bases: BaseCamera, BaseDataProducer

Synthetic camera producing real OME-TIFF + frame metadata JSON.

__init__(cfg=None, **kwargs)[source]#
Parameters:
Return type:

None

arm(config)[source]#

Per-run prep: set up the writer + (optionally) an MDA sequence.

The default body fits both MMCamera (which needs a sequence) and OpenCV/Mock (where set_sequence is a no-op). Subclasses override only when they need additional prep.

Parameters:

config (ExperimentConfig)

Return type:

None

snap()[source]#

Capture a single frame outside any recording, return it as an ndarray.

Used by the GUI’s snap button. Implementations should NOT alter recording state – snap is preview-only – but they SHOULD call _save_snap_png() so each snap also lands a *_snap.png.

Return type:

ndarray

start_live()[source]#

Begin continuous live preview WITHOUT writing to disk.

Subscribers receive frames via image_ready (Qt) / signals.data (psygnal). No recording side-effects; pair with stop_live().

Return type:

None

stop_live()[source]#

End the continuous live preview started by start_live().

Return type:

None

save_data(path=None)[source]#

Replay buffered frames through the standard mesofield writer.

Parameters:

path (str | None)

Return type:

str | None

property calibration: Dict[str, Any]#

Camera-specific constants worth recording in the AcquisitionManifest.

Subpackages#

Submodules#