Skip to content

projects/ janelia/ Mouse Joystick/

pyControl Interface

Rev 0 scope: the serial link, plus pin 6's 5 V as one of the two sources that power the board at all. Neither the UART nor the 5 V branch passes through the GPIO isolation switch, so the board stays reachable over pyControl with USB unplugged and the motor rail switched off — that is a deliberate property of the power architecture, not an accident. See docs/power.md.

Otherwise: the serial link only. PULL_EVENT, FAULT, and RSYNC and their BNCs are deferred to rev 1, so Rev 0 has no hardware-timed event or fault path — every event is a UART line with UART latency. Fine for exercising the interfaces, not fine for behavioral data, and that is the boundary between Rev 0 and rev 1. The cross-board EXT_FAULT_IN discussion below is also rev 1: Rev 0 has no FAULT output to cross-wire.

Status: Pinout and serial parameters established from a cited working design; electrical properties unverified. Date: 2026-08-12 Companion: docs/pycontrol-port-pinout.md holds the established pinout, UART parameters, and UART-capable port list. Read that first; this file covers how this board uses them. Derived from: mouse-joysticks-pcb/docs/pycontrol-interface.md Gate: Must reach a resolved state to pass Gate A in docs/validation-plan.md.

The Rule

RJ-45 names a connector family, not an interface. It establishes nothing about pin numbering, orientation, cable wiring, voltage, current limits, startup state, or allowed signal use.

pyControl's behavior-port cabling is a Cat5/RJ-45 wiring convention, not Ethernet networking. There is no PHY, no magnetics, and no MAC. Do not place Ethernet magnetics, and do not let anyone describe this as an Ethernet port in a design review.

Nothing here may be treated as verified until it cites the authoritative document for the deployed hardware revision, or a bench measurement.

One UART Per Board, One Board Per Joystick

This board is a single-axis peripheral. There is no axis argument in the protocol, no axis field in events, and no cross-axis policy.

A two-board rig therefore consumes:

Two single-axis boards One twin-axis board
Behavior ports 2 1
UART links 2 1
TTL signal lines 6 4
Instruments to see both axes 2 1

That comparison is the cost of this form factor, recorded in full in docs/relationship-to-twin-axis-board.md. It is accepted here in exchange for schedule.

Signal Set

Signal Direction Electrical Where
UART_TX board → pyControl 3.3 V TTL, 115200 8N1, newline-framed RJ-45 pin 4 DIO-B
UART_RX pyControl → board 3.3 V TTL, 115200 8N1, newline-framed RJ-45 pin 1 DIO-A
PULL_EVENT board → pyControl clean TTL pulse BNC
FAULT board → pyControl latched TTL BNC
RSYNC pyControl → board TTL edges BNC
GND shared — RJ-45 pin 2, BNC shells

Rules:

  • Event lines exist because pyControl timestamps edges with lower latency than it can parse a UART message. Anything latency-critical is a line; anything descriptive is a message. TARGET_REACHED is a message, not a line.
  • Configure pyControl digital inputs for these lines with debounce disabled; they are clean electrical edges, not mechanical switches.
  • Keep motor power off this connector. The board runs a 36 V rail the port does not supply, and stepper current must not share a bundle with the UART carrying the safety heartbeat.
  • Add series resistors and ESD protection on every port signal.
  • Define behavior for every connection order: board powered first, pyControl powered first, cable inserted live, cable removed live.

Two Boards Is A Software Question Too

The syringe pump's device class shape:

from machine import UART
from devices.uart_handler import UART_handler

class PumpController:
    def __init__(self, port, event_name, pybv1=True):
        assert port.UART is not None, "! Pump needs port with UART."
        self.uart = UART(port.UART)
        self.uart.init(115200, bits=8, parity=None, stop=1, timeout=100, rxbuf=130)
        handler = UART_handler(event_name)
        self.uart.irq(trigger=UART.IRQ_RXIDLE, handler=handler.ISR)

For this board the natural idiom is direct — one device object per board, which is exactly what the template does:

left  = MouseJoystick(port=board.port_1, event_name="joystick_L",
                      pull_pin=board.BNC_1, fault_pin=board.BNC_2)
right = MouseJoystick(port=board.port_4, event_name="joystick_R",
                      pull_pin=board.BNC_3, fault_pin=board.BNC_4)

hw.left.home()
hw.right.move_to(-10.0)

The BNC_* arguments above are illustrative and do not exist. Breakout 1.2 has exactly two BNCs, BNC_1 and BNC_2, and both are already in use — camera frame trigger and opto TTL gate. There is no BNC_3 or BNC_4. The rev-1 event path needs six such lines and this controller has none free; see the Gate A row on BNC count. The ports are real: 1 and 4, per docs/pycontrol-port-pinout.md.

This is the one place the two-board form factor is simpler in software. The twin board has to answer whether one pyControl device class can present two axes cleanly, or whether the natural idiom is two axis objects sharing one transport — an open question in that repository. Here the answer is the template's answer, unmodified.

Two things to verify anyway:

  • Two instances of the same device class on two ports must not collide on event names or on the UART_handler registration. Give each instance a distinct event_name.
  • Both instances must receive the same RSYNC edges. See the fan-out question in docs/pycontrol-port-pinout.md.

Tasks should read at experiment altitude, not pin altitude.

Gate A Verification Checklist

Every row needs a status of verified, cited, or unknown, plus the source.

Item Status Source
RJ-45 pin assignment cited syringe-pump/ecad/module_pycontrol.kicad_sch
Which pins carry the UART (1 = pyControl TX, 4 = pyControl RX) cited same schematic, net trace
UART parameters (115200 8N1, newline, ;-delimited) cited pump_controller.py, basic-firmware/code.py
Which ports expose a UART verified Queried on the live controller 2026-09-03: ports 1/3/4 report UART = 4/1/3, ports 2/5/6 report None
Number of DIO lines per behavior port (two) cited port pin assignment
Native custom-device mechanism cited pump_controller.py uses devices/ + UART_handler + UART.IRQ_RXIDLE
Exact pyControl controller model and hardware revision verified PYBv1.1 with STM32F405RG, read from the board 2026-09-03; pyControl framework 2.1.1
Which breakout the lab actually deploys cited Breakout 1.2 — joystick-rig hardware_definition_joystick.py instantiates Breakout_1_2(), and it imports on the connected board. Silkscreen revision not read by hand
Port designations assigned to the two boards assigned; one dependency unconfirmed Ports 1 (UART4) and 4 (USART3), 2026-09-03, and set in mouse_joystick_bringup.py. Port 3 belongs to LaserLickPort — the only port with I2C and a free DIO_C. Port 4 depends on the reward solenoid moving to port_5.POW_A, requested of the researcher 2026-09-03 and not yet confirmed; until then port 4 is contested. Full reasoning: docs/pycontrol-port-pinout.md
BNC count and direction configurability — six lines needed verified, and short Breakout 1.2 has two BNCs; BNC_1 is the camera frame trigger and BNC_2 the opto TTL gate as of joystick-rig source/main. Six lines do not fit — this needs a plan before rev 1
Whether one RSYNC output can drive two boards unknown
Connector gender and orientation on both ends unknown
Cable wiring: straight-through or crossover unknown
DIO logic level, tolerance, and drive strength unknown
Whether DIO lines are 5 V tolerant unknown
Port +5 V current budget unknown
Shared regulator limit across all ports unknown
SPECIAL pin behavior, and whether it can serve as a TTL line unknown
Startup and reset state of every line unknown
Grounding topology, and whether isolation is needed unknown
Digital-input debounce behavior and how to disable it unknown
RSYNC output electrical characteristics and pulse timing unknown
Two device-class instances coexist without event-name collision unknown
Controller MicroPython version >= 1.26 verified UART_handler asserts on it. Board was on 1.22.2; upgraded to 1.26.1 2026-09-03. See below

Everything marked cited comes from a shipping pyControl-compatible design, which is strong evidence but not a bench measurement. Confirm continuity, levels, and startup state on real hardware at Gate B.

Design for 3.3 V signaling unless level shifting or isolation is deliberately added.

Controller Prerequisite: MicroPython >= 1.26

Found 2026-09-03 and previously recorded nowhere. This is a hard gate on the host half, independent of anything on this board.

pycontrol/code devices/uart_handler.py opens with:

assert (major > 1) or (major == 1 and minor >= 26), "UART_handler requires MicroPython version >= 1.26"

introduced in pyControl 2.1.1 — its release notes say "Simplified UART_handler device; now requires MicroPython >= 1.26" — and stated in the pyControl hardware docs as "The UART_handler device requires MicroPython version 1.26 or newer."

MouseJoystick.__init__ constructs UART_handler(event_name) and then calls uart.irq(trigger=UART.IRQ_RXIDLE, ...), so on an older controller the device class cannot be instantiated at all — it asserts before any wiring, baud rate or protocol question can be reached. The pyControl framework version on the board is no guide here: the board carried framework 2.1.1 on MicroPython 1.22.2, a combination in which UART_handler cannot run.

Resolved on the bench controller 2026-09-03: MicroPython 1.22.2 (2024-02-22) → 1.26.1 (2025-09-11), standard PYBV11 build. The pyboard's internal filesystem occupies flash sectors 1-4 and the firmware image writes only sector 0 and sectors 5+, so the framework, devices/ and the hardware definition all survived the flash — verified afterwards, not assumed. UART.IRQ_RXIDLE reads 0x10, matching the value the host test stubs assume.

Two consequences worth carrying forward:

  • The rig's own controller has not been checked. The board upgraded here is a bench pyboard carrying a rotary-encoder test task, not hardware_definition_joystick.py. Check the rig controller's version before integration, and expect to upgrade it too.
  • Take the standard PYBV11 build, not THREAD or double-FP; the pyControl troubleshooting guide says so and the framework uses no _thread.

Heartbeat And Safe State

The board requires a periodic host heartbeat over UART. On timeout it stops motion, latches a fault, asserts FAULT, and drops HV_EN.

This is deliberately independent of pyControl. A task that crashes, a USB cable that is unplugged, or a host that is put to sleep must not leave the motor energized against a mouse. The board is the last line of defense, so its safe state must not require a message to arrive.

With two boards, a heartbeat failure stops one joystick, not both. That is this form factor's genuine safety advantage over the twin board — and also its genuine hazard, because a half-stopped rig is a state the twin board cannot enter.

Decide explicitly what happens to the other joystick. Options:

  1. pyControl's task detects the fault event and stops the other board. Simple, but it depends on host cooperation, which is exactly what the local safe state exists to avoid depending on.
  2. Cross-wire the two boards' FAULT lines so either board's fault reaches the other as an external stop input. This board has three ADC-capable spare GPIOs; spending one on an EXT_FAULT_IN input is cheap and does not depend on the host. Recommended if the rig runs two boards.
  3. Accept single-axis degradation, with the resulting data explicitly marked. Only as a reviewed scientific choice.

If option 2 is chosen, decide it before layout — it adds a connector pin, and retrofitting it means a flying wire on a rig board.

Calibration Boundary

Per-rig parameters — encoder offset, angle zero, travel limits — are stored on the board and reported to pyControl at run start.

With one board per joystick, each board holds exactly one calibration set, which removes the twin board's risk of silently applying one axis' calibration to the other. In exchange it introduces a new one: a board swapped between joysticks carries the wrong calibration with it. Record the board serial against the joystick it is calibrated for, and have the board report its serial in ID so the task can check.

pyControl-facing values the experimenter should change per setup belong in pyControl hardware variables instead.

Open Questions

  • Which port designations carry the two joystick UARTs, and is that recorded anywhere the lab can see it?
  • Does the deployed pyControl version's UART handler generate a framework event on message receipt, or must the task poll?
  • Are there six usable BNC lines on the deployed breakout?
  • Can one RSYNC output drive two boards' inputs?
  • Cross-board fault propagation: host-mediated, hard-wired, or none?
  • Should the two boards' grounds be tied at the pyControl controller, at the rig frame, or isolated? With two boards this is a ground-loop question that the twin board does not have.
  • Does the rig need a physical emergency-disable independent of both pyControl and this firmware? With two boards it must cover both.

Promotion

Once verified, the reusable physical and electrical pyControl connector profile belongs in board-interfaces, not here. This repository documents only how this board uses that profile.