This file is the canonical project guidance for agent sessions in this repository.
Session Start
Read this file first.
If AGENT_MEMORY_REPO is set and points at a directory, read
$AGENT_MEMORY_REPO/AGENTS.md before planning or editing.
If sibling checkouts exist, read:
../mouse-joysticks-pcb/docs/— the twin-axis board for the same rig, and the source of most of this repository's documentation../orb-weaver-context/context/agent-index.md../orb-weaver-context/context/kicad-agent-context.md../orb-weaver-context/context/pcb-development-plan.md../board-interfaces/docs/instrument-port-40.md../kicad-libs/docs/design-blocks.md
If .agents/handoff.local.md exists, read it before planning or editing.
If the task is multi-step, risky, or likely to span more than one session, read
.agents/PLANS.md and keep it updated as work progresses.
Current State — 2026-10-15
v1.0 is fabricated, tagged v1.0 and in service: four boards built from
fab/mouse-joystick-pcb/ (the v1.0 order, regenerated only for a new
revision), two in Rig 1 and two on the verification rig. The copper is the
fabricated copper; since the tag the block sheets were regenerated from the
library with the copper kept, and the PCB changed in footprint fields, paths
and group ids only. Its documentation is complete: the generated set under
docs/generated/ (block pages, subsystem pages, the MCU pin allocation, the
dimensioned outline drawing, the BOM with its sourcing, the recorded netlist
checks), the README block and the Pages site
(https://peterpolidoro.github.io/mouse-joystick-pcb/, built by
.github/workflows/pages.yml from the committed pages; make site builds it
strictly here). Bring-up findings and open items are in .agents/PLANS.md
and docs/bringup-log.md.
The routing record of September 2026 (the routing session handoff, the autoroute log, the routing completion and the connector access correction) is history, kept for how the board came to be; it is not an instruction to route again. The next revision's changes are the PLANS items (the brake overshoot, the inert torque calibration switch, the USB 1200 baud touch), each decided with measurements first.
Repository Purpose
This repository owns the KiCad implementation and board-specific notes for
mouse-joystick-pcb: a single-axis mouse joystick controller integrated as a
native pyControl peripheral. Two of these boards serve the rig's two joysticks.
There is a second, live design for the same rig. ../mouse-joysticks-pcb is
the twin-axis board. The form-factor question is deliberately open. See
docs/relationship-to-twin-axis-board.md.
The predecessor motor-control validation board is ../tmc51x0-driver-dev-pcb.
The previous-generation joystick board is ~/Repositories/kicad/mouse-joystick-pcb
— same name, different design, prior art to mine rather than a dependency.
Board Rules
- One PCB, one joystick axis, one pyControl UART. There is no axis argument in the protocol and no axis suffix in net names. If you find yourself adding one, you are working on the wrong board.
- Do not port twin-axis complexity here. Axis symmetry rules, cross-axis
fault policy,
XFAULT, and theB-for-both protocol argument belong in../mouse-joysticks-pcb. Their absence is the point. - Rev 0 is deliberately scoped down; the pin map is not. Rev 0 connects 18
of 30 GPIOs and reserves more at fixed numbers so rev 1 adds them without
moving anything. Spares are now
GP15,GP16,GP18andGP27— four, not five.GP14was consumed on 2026-08-17 byHV_DIAG_EN; see theDIAG_ENrule below. If a change needs more pins than are free, say so rather than quietly cutting a reserved function. Seedocs/interface-map.mdanddocs/rev0-plan.md. kicad/board-manifest.yamlis the board's manifest (kicad-libs decision D20, written 2026-10-01): every placed block with its anchor, each port mapped to the label it meets in its parent sheet oropenwith a reason, the four wrapper sheets, the netclass profiles and every net's class. After placing, wiring or moving a block:make mechanical-datums,make board-manifest-derive(keeps the written meanings and reasons),make board-manifest-check. The block between theGENERATED BOARD MANIFESTmarkers at the top ofdocs/interface-map.mdis its rendering (make board-manifest-render), never edited by hand; the narrative below the block is.make composemakes the project agree with the manifest (kicad-libs D28, proven on a scratch copy of this board 2026-10-02): a placement the board lacks gets its child sheet from the library block, its sheet symbol with the recorded pins, a stub and label per mapped port, a no-connect per open port and its PCB group with the block's copper at the recorded anchor; a pin wired to nothing gets its stub. It never moves, renames or deletes, so a difference it reports is fixed in the drawing or the manifest.make compose-checkis the dry run; after a real run,make mechanical-datumsthenmake board-manifest-check. The scratch proof lives intmp/compose-proof/(ignored) until the GUI gate is done.kicad/board.yamlis the board's source (kicad-libs D27, written 2026-10-02): themotor_encoder_switchrecipe from kicad-libs by reference (pinned by hash; thedriverrole overridden to the as-built retiredtmc5130block, which the expansion reports as drift), the three board-local recipes inkicad/subsystems/(mcu,brake_torque_sensor,instrument_port), the five root blocks, and the net meanings;kicad/sheet-pins.yamlholds this board's own sheet-pin arrangements.make expandwriteskicad/board-manifest.yamlfrom it andmake expand-checkcompares (exact as of 2026-10-02). The manifest stays the checked file: edit the source,make expand,make board-manifest-check, thenmake compose.make refresh-block INSTANCE=<name> [BLOCK=] [RENAME=OLD=NEW]brings one placed block up to its library block (kicad-libs D22, proven on a scratch copy of this board 2026-10-02: the 5 V regulator toorb_weaver_tpsm365r6_fixed:tpsm365r6_fixed_5v, whose output port isVOUTwhere the board says+5V; the digital-50 boundary's copper). It keeps designators, restores pins by name, replaces the group's copper by identity and leaves everything else byte for byte; it prints what to change inkicad/board.yaml. The three retired blocks (tmc5130→orb_weaver_tmc5130:tmc5130_spi_r150,hv_to_5v_tpsm365r6,hv_to_12v_tpsm365r6→orb_weaver_tpsm365r6_adjustable:tpsm365r6_adjustable_12v) are its targets on the real board, when you decide to; the regulators' new port copper lands on your own routing (DRC names it).make generate-gluedraws this board's own parts, the glue a recipe orkicad/board.yamlnames (the pulls and series resistors, the connectors, the mounting holes, the PWR_FLAGs), into the sheetsmake composecreated: a symbol with a stub and a local label per pin carrying the recipe's net name, two-pin parts upright in a row with reference and value at the right (kicad-libs step 8b, D28; proven 2026-10-02 by regenerating this whole schematic intotmp/generate-proof/to the hand-built netlist). It creates and never moves: a part already drawn under the recipe's reference, or placed by an earlier run, is skipped, so on the drawn board it has nothing to draw. Designators of parts it draws follow the sheet's page band; the recipe-to-board mapping is inkicad/board-manifest.compose.json.GENERATE_GLUE_OPTIONS=--dry-runreports first. The order for a new board: edit the source,make expand,make compose,make generate-glue,make board-manifest-check.- Current sense is 0.15 Ω with
vsense = 1andIRUN = 28for the 0.67 A/ phase motor, on both shunt positions. Thetmc5130block ships 0.12 Ω as its default — change it. As of 2026-08-17 this is the only electrical difference between the board and itskicad-libssources, and it runs in the board's favour:kicad-libsmoved to 0.12 Ω in commita8dca31. Re-placing thetmc5130block silently reverts the shunts and puts roughly 25% over the intended current. Fix the library down to 0.15 Ω, or never re-place that block.vsenseis CHOPCONF bit 17 and its reset default is 0; leaving it at 0 with 0.15 Ω installed drives roughly 1.8× the motor's rated current. Firmware must set and read backvsense = 1before raisingDRV_ENN. The shunt value and thevsensesetting are one decision — never change either independently. - The supply is 36 V, and that has consequences. The TMC5130A operates to
46 V, so 36 V leaves 10 V of transient headroom, and the 36 V input block
populates no TVS. The TMC's internal 5 V regulator must be offloaded by
supplying
VCCexternally. Seedocs/power.mdbefore touching the power section. - The high-side switch is a reset button, and the board has two power
domains. Feed
hv_to_5v_tpsm365r6from the switched+36V_SW— that is what makesHV_ENpower-cycle every resettable chip. It is only safe because the MCU is on a separate always-on domain fed from USBVBUSor pyControl pin 6 through two ORing diodes, never from the 36 V chain. Reversed 2026-08-13; earlier guidance said feed pre-switch. Putting the MCU on the switched chain is still the way to make an unbootable board. Seedocs/power.md. DIAG_ENmust stay on a GPIO. Never tie it high. Added 2026-08-17, and it is not obvious: holding the TPS1HTC100'sDIAG_ENhigh keeps its OFF-state open-load detection enabled, which switches in an internal 150 kΩ pull-up fromVStoVOUT. At 36 V that trickles ~240 µA into+36V_SW's ~208 µF, the rail creeps past the buck's 3.6 V minimumVINin about three seconds, and the switched domain hiccups instead of staying off — which silently destroys the power-cycle-as-reset property above.DIAG_ENhas an internal pull-down, soGP14high-Z at reset means diagnostics off and+36V_SWat true zero. Protection still works with diagnostics off; only reporting is gated, so firmware must raiseGP14before reading current sense or faults.- Anything that must read HIGH while the driver is powered pulls up to
+3.3V_SW, never+3.3V_MCU. Pulling a cross-domain net up to the always-on rail back-powers+3.3V_SWto roughly 1.3 V through the TMC's protection diodes, defeats theHV_ENpower-cycle, and corruptsSW_RAIL_SENSE— the very reading firmware uses to decide the switched rail is down. This was built the wrong way once. Pulls that may read LOW go straight to GND. The nine board-level termination resistors and the reasoning for each are in.agents/PLANS.md. +3.3V_SWis held at 0 V when off byR401/R402, theSW_RAIL_SENSEdivider. That is what makes a pull-up to that rail a valid LOW in the off state. Delete the divider and four termination resistors silently stop working.- pyControl owns experiment sequencing. This board owns local motion timing and safety. Never move experiment-level decisions into this firmware, and never expose unbounded trajectories or raw motor power as the normal API.
RJ-45names a connector family, not an interface. Do not treat the pyControl behavior port as Ethernet and do not place Ethernet magnetics. The pinout and serial parameters indocs/pycontrol-port-pinout.mdare cited, not bench-verified.- The assigned pyControl port must be UART-capable: ports 1, 3, or 4 on Breakout
board 1.2; ports 1, 2, 3, 4, 5, 9, or 10 on the H7 breakout — but the H7's
UARTs are routed, not yet declared, because
pycontrol/codev2.1.1 has nobreakout_h7.py. The D-series Breakout Board 1.6 list (8, 10, 11, 12) is carried forward unverified; that board is in no local checkout. The full controller-side map is../board-interfaces/docs/pycontrol-behavior-port.md. - Both DIO lines on the assigned port are consumed by the UART. The three TTL lines land on BNCs in rev 1 — Rev 0 carries the serial link only.
DIO-Ais this board's RX;DIO-Bis this board's TX. The alphabetical order reverses, soPYC_DIO_A→PYC_UART_RX→GP1andPYC_DIO_B→PYC_UART_TX→GP0. Block pins are named by connector position, board nets by MCU function; the crossover happens once, at themcusheet boundary. Diagram and the full sheet-pin direction table indocs/interface-map.md.- pyControl pin 6's 5 V is load-bearing, not optional: it is one of only two sources that power the board. Confirmed 5 V from the syringe-pump netlist; its current budget is unmeasured.
- Do not carry motor power through the pyControl behavior port. The board runs a 36 V rail the port does not supply.
- InstrumentPort-40 is in Rev 0 as of 2026-08-16, with 11 of 18 digital
Strands live and 4 reserved for the rev 2 brake and torque sensor — three of
which
ENC_A,ENC_B, andLIMIT_SWborrow until then.DIG0,DIG1, andDIG11are swap slots; do not treat those tenants as permanent, and do not reassign the slots early. The full allocation is indocs/interface-map.md. It is intended for use during scientific experiments, not only on the bench, so observation must be passive, unidirectional, and unable to affect rig behavior. No divided motor rail reaches anyAIN*. - The encoder and the limit switch never reach a GPIO, in any revision —
decided 2026-08-16. The TMC5130A owns them; firmware reads position and homing
from the driver over SPI.
GP14/GP15/GP16/GP18are spare and the PIO cross-check andENCODER_MISMATCHare deleted, not deferred. The InstrumentPort taps are the board's only independent witnesses to those signals. - Tap points are decisions, not details. The TMC5130A bus is observed on the
A side of the GPIO isolation switch, and the encoder and limit switch on the
driver side of their conditioning, because in those cases the series element
has two distinguishable states. The pyControl serial is taken from the port block's
existing
PYC_DIO_A/PYC_DIO_Bpins — do not re-propose a connector-side tap with extra boundary pins; it was considered and rejected, because a digital channel cannot resolve 33 mV across 100 Ω and the tap would sit on the unprotected side of the ESD clamp. All reasoned indocs/interface-map.md. A 100 kΩ pull-down goes wherever a tapped net can float, and noSN74LVC125Ainput may be left undriven. - Do not redefine InstrumentPort-40 here; document only this board's overlay. If
the profile is genuinely too narrow, raise it in
board-interfaces. - Do not define the reusable pyControl connector profile here. That belongs in
board-interfacesonce it has sufficient evidence. - Two interchangeable boards in one rig is a new failure mode. Calibration
belongs to a board serial, not to a joystick position.
IDreports the serial. Silkscreen a unit identifier. - Rev 0 has no SWD header and no reset connection, decided 2026-08-17.
RUN,SWCLKandSWDare no-connect flagged. RP2040RUNhas an internal 50–80 kΩ pull-up so this is an access limitation, not an electrical defect — but it means no debugger during bring-up, and BOOTSEL recovery needs both USB and pyControl unplugged, because the MCU domain is ORed from two sources. Revisit in a future revision rather than treating it as an oversight. - Distinguish cited claims from bench-verified claims. Unknowns stay explicit unknowns with an owner.
- Use
kicad-libsfor shared symbols, footprints, internal parts, vendor mappings, and design blocks. Promote a circuit intokicad-libsonly after this board verifies it. - Record real board results in
docs/bringup-log.md, per board serial.
Keeping Two Repositories In Sync
Shared facts live in both this repository and ../mouse-joysticks-pcb:
- the pyControl port pinout and UART parameters
- the motor part, its rating, and the current-sense decision
- the encoder part and the limit switch part
- the DAC part and the logic-level answer
- the UART protocol's framing, verbs, events, and fault model
When one of these changes, change it in both. A silent divergence in a shared fact is the failure mode of running two repositories.
Safety Rules
The board must reach a safe state without pyControl's cooperation:
- On boot: motor rail off, stepper outputs disabled, wait for an explicit
ARM. - During operation: require a host heartbeat; enforce current, velocity,
acceleration, and travel limits; latch faults; assert the
FAULTline; dropHV_EN. - On host loss, heartbeat timeout, reset, or fault: stop motion, report the cause.
- With two boards in a rig, one board's fault leaves the other running. Decide
explicitly whether that is acceptable, and prefer a hard-wired
EXT_FAULT_INover relying on the host to notice. Seedocs/pycontrol-interface.md.
Development Environment
Prefer Makefile targets:
make test— documentation and scaffold presence checkmake codex/make claude— agent sessions with sibling checkouts addedmake agent-reflectionmake kicad-shell/make kicad-editsmake validate-db— Orb Weaver KiCad database library wiringmake fab— fabrication outputsmake docs— regeneratedocs/generated/and the README's managed blockmake docs-check— report whether that documentation is stalemake check-kicad-libs
docs/generated/ and the block between the
<!-- BEGIN/END GENERATED BOARD DOCUMENTATION --> markers in README.md are
written by make docs. Never hand-edit either. make docs is a no-op unless a
.kicad_sch, .kicad_pcb, .kicad_pro or .kicad_dru hash changed, so run it
freely; make docs-check is the pre-tag gate and names what moved. Mechanical
coordinates there are centre-of-board origin, X right and Y up, which is not
KiCad's frame — see kicad-libs/docs/board-documentation.md.
Local KiCad Overlay
The pinned Guix channel (dd8c1c5) carries KiCad 10.0.6, verified with
guix time-machine -C .guix/channels.scm -- show kicad, so plain
make kicad-edits gives 10.0.6. The opt-in overlay in
.guix/manifest-kicad-local.scm and
.guix/local-modules/orb-weaver/kicad-local.scm (make kicad-version-local,
kicad-shell-local, kicad-edits-local, fab-local) stays as the hook for
the interval between a KiCad release and its Guix package; the user uses it
whenever KiCad is ahead of Guix. Since 2026-10-08 it overrides KiCad 10.0.7
(released that day; the Guix update is submitted upstream), so the -local
targets give 10.0.7 and the default ones 10.0.6. The module is byte-identical
in kicad-libs, mouse-joysticks-pcb and tmc51x0-driver-dev-pcb; bump all
four together (the procedure is in kicad-libs/docs/guix-packaging.md), and
reset it to plain aliases once the pin catches up.
Routing Helpers
make export-dsn, make freeroute, make import-ses, make clearance-baseline
and make clearance-check call the shared kicad-libs commands
(orb-weaver-kicad-export-dsn, -import-ses, -clearance-check); the scripts
moved out of this repository's scripts/ on 2026-09-25 and the channel pin
768a00b ships them. kicad/clearance-baseline.json is still this board's
baseline. Workflow: kicad-libs docs/freerouting-autorouting.md.
KiCad Project State
Layout and routing are complete as of 2026-09-21. The four-layer PCB has 364 footprints, with all 1,204 numbered pad identities matching the schematic. The configured fabrication build reports ERC 0 errors/0 warnings and DRC 0 errors/0 unconnected pads. Its remaining warnings are 499 advisory clearances and 20 dangling vias; clearance regression passes with no voltage-bearing findings. These checks do not establish firmware or bench behavior.
ERC alone did not catch the earlier electrical findings. After schematic or
layout edits, compare the exported schematic netlist to the PCB as well as
running ERC/DRC. Use .agents/PLANS.md and the tracked handoff above for current
status; older dated planning entries are history where completion supersedes
them. The project now has 24 netclasses and 166 netclass patterns, the four-layer
stackup, plot/fabrication setup and JLCPCB design rules.
The netclasses were doing nothing until 2026-09-17. The .kicad_dru carried
an unconditional (constraint clearance (min 0.10mm)) rule, and an
unconditional custom clearance constraint replaces the netclass value rather
than flooring it — so the effective clearance was 0.10 mm board-wide and all 21
classes' 0.15–0.5 mm settings were dead letters. Fixed in ff081ff: the fab
floors moved to Board Setup → Constraints (min_clearance and min_track_width,
0.0 → 0.1 mm) and both unconditional rules were deleted. Never re-add a fab
floor as an unconditional rule — the file carries a comment saying so. All
conditional rules were kept.
rule_severities.clearance is error here and must stay that way. The
existing scoped Legacy low-voltage block clearance is advisory rule demotes
non-voltage-bearing pairs to warnings; HV, motor and other voltage-bearing
classes remain errors. Preserve that distinction and run make clearance-check
after routing. Never suppress the clearance category globally to hide failures.
The inherited patterns were generic names (+24V, VM, PGND, +3V3,
USB_HS) that matched almost nothing here — 146 of 156 nets fell to Default at
0.2 mm, including the 36 V rail and all four motor phases, and */HV_IN was
silently matching PWR_HV_LOCAL at 0.25 mm. Fixed 2026-08-17. Two things to
know before touching them again: this board's rails are spelled +3.3V_*, not
+3V3_*; and BRA/BRB are not Kelvin sense nets on a TMC5130A — they carry
full coil current into the sense resistor, so they are sized with the motor
phases. SENSE_KELVIN exists for the TMC5160's SRA/SRB. If you add patterns,
re-check that no net matches more than one netclass.
Validation
For documentation-only changes, review against docs/validation-plan.md gates.
For schematic/layout work, use KiCad ERC/DRC once the design is mature enough for those checks to be meaningful.
ORB_WEAVER_KICAD_LIBS must resolve for KiCad to find the database and footprint
libraries.
When editing kicad-libs design blocks, run scrub-private-vendor-fields
then validate. Validate inspects .kicad_sch only, so a passing validate
does not prove the .kicad_pcb side is clean.
Publication Layers
This repository is mirrored to GitHub for the rig's researchers, so it has two layers (decided 2026-10-12):
- Citable layer:
README.md,CITATION.cff,LICENSE,LICENSES/,kicad/,fab/,docs/generated/and every document the README lists. It must open and read without any Orb Weaver checkout and must not refer to codeberg.org or toorb-weaver-*repositories. KiCad library identifiers (orb_weaver_private:…,orb_weaver_smd:…) and the generated-block comments are allowed; they are library references, not links. Files in other repositories are cited as<repository>/<path>with no../and no URL;docs/development.mdmaps each repository name to its URL. - Development layer:
docs/development.md, the documents it lists,AGENTS.md,.agents/,.codex/,Makefileand.guix/. These may name the sibling checkouts and the Orb Weaver repositories.
A new document goes into exactly one of the two lists. LICENSES/NOTICE.md
keeps its kicad-libs URL: it is the CC-BY-SA attribution for the embedded
symbols, footprints and blocks.