Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,21 @@ jobs:
run: npm test --prefix packages/tiny-engineer-antigravity
- name: Claude Code package tests
run: npm test --prefix packages/tiny-engineer-claude-code

pcb:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Install KiCad 10
run: |
sudo add-apt-repository -y ppa:kicad/kicad-10.0-releases
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends kicad
kicad-cli version
- name: ERC / DRC / expected-nets
run: python3 scripts/check_pcb.py --report-dir artifacts/pcb
- uses: actions/upload-artifact@v4
if: failure()
with:
name: pcb-reports
path: artifacts/pcb/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.DS_Store
__pycache__/

# Cursor hook event log
cursor_events.log
Expand Down
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ pio run
pio test -e native
npm test --prefix packages/tiny-engineer-cursor
npm test --prefix packages/tiny-engineer-antigravity
python3 scripts/check_pcb.py
```

`pio test -e native` is not `pio run -e native`. Details: [docs/testing.md](docs/testing.md).
`pio test -e native` is not `pio run -e native`. PCB checks need KiCad 10 (`python3 scripts/check_pcb.py`). Details: [docs/testing.md](docs/testing.md).

Do **not** flash (`pio run -t upload`) unless the user asks.

Expand Down Expand Up @@ -66,7 +67,7 @@ Breaking rules, SemVer mapping, and examples: [CONTRIBUTING.md](CONTRIBUTING.md)
- **Settings** — layer checklist in [docs/settings.md](docs/settings.md). Never log raw `access_token`.
- **Integrations** — add/extend package tests; prefer short timeouts and ignore network errors so a missing robot does not stall the agent.
- **CAD** — edit `.f3d` **and** export affected `3mf`. CERN-OHL-S. Do not swap `AiEmblem.3mf` as branding.
- **PCB** — [docs/pcb.md](docs/pcb.md) checklist + KiCad review rules below. New board paths need `REUSE.toml`. CERN-OHL-S.
- **PCB** — [docs/pcb.md](docs/pcb.md) checklist + KiCad review rules below. Run `python3 scripts/check_pcb.py` (KiCad 10). New board paths need `REUSE.toml`. CERN-OHL-S.
- **Motion** — poses −1..1 mapped to saved min/max; see [docs/robot-movement.md](docs/robot-movement.md). Do not widen NVS servo clamps without testing on a real robot.
- **Secrets** — no `.env`, tokens, or Wi-Fi passwords in logs or screenshots.

Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ pio run
pio test -e native
npm test --prefix packages/tiny-engineer-cursor
npm test --prefix packages/tiny-engineer-antigravity
python3 scripts/check_pcb.py
```

`pio test -e native` is not `pio run -e native`. Details: [docs/testing.md](docs/testing.md). Flash locally with `pio run -t upload` when you have a board. Physical module is a **Waveshare ESP32-C3-Zero**; PlatformIO `board = esp32-c3-devkitm-1` is the build target name.
`pio test -e native` is not `pio run -e native`. PCB checks need KiCad 10 (`python3 scripts/check_pcb.py`). Details: [docs/testing.md](docs/testing.md). Flash locally with `pio run -t upload` when you have a board. Physical module is a **Waveshare ESP32-C3-Zero**; PlatformIO `board = esp32-c3-devkitm-1` is the build target name.

## By change type

Expand All @@ -56,7 +57,7 @@ npm test --prefix packages/tiny-engineer-antigravity

**CAD.** Edit [`3d_models/cad/TinyEngineer.f3d`](3d_models/cad/TinyEngineer.f3d) **and** export the affected [`3d_models/parts/{servo_id}/3mf/*.3mf`](3d_models/parts/). Keep CERN-OHL-S. Do not swap `AiEmblem.3mf` as a branding change.

**PCB.** Follow the [PCB checklist](docs/pcb.md#checklist). Keep [`expected-nets.yml`](docs/pcb.md#expected-netsyml) in sync. One board per `hardware/boards/<name>/`, KiCad 10, ERC and DRC reviewed, no generated Gerbers or other fab outputs. Keep CERN-OHL-S. New board paths need a matching `[[annotations]]` block in [REUSE.toml](REUSE.toml).
**PCB.** Follow the [PCB checklist](docs/pcb.md#checklist). Run `python3 scripts/check_pcb.py` before opening a PCB PR. Keep [`expected-nets.yml`](docs/pcb.md#expected-netsyml) in sync. One board per `hardware/boards/<name>/`, KiCad 10, ERC and DRC reviewed, no generated Gerbers or other fab outputs. Keep CERN-OHL-S. New board paths need a matching `[[annotations]]` block in [REUSE.toml](REUSE.toml).

**Motion.** Animations use −1..1 poses mapped to the saved min/max in [docs/robot-movement.md](docs/robot-movement.md). Stock defaults live in [`include/servos.h`](include/servos.h). Do not widen NVS servo clamps without testing on a real robot. Setup AP `POST /setup/servo` can use 0–180° to find limits; assembled motion must not.

Expand Down
5 changes: 5 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ path = "hardware/boards/**"
SPDX-FileCopyrightText = "2026 Krzysztof Jamroz"
SPDX-License-Identifier = "CERN-OHL-S-2.0"

[[annotations]]
path = "hardware/boards/main-control-board/**"
SPDX-FileCopyrightText = "2026 Krzysztof Jamroz"
SPDX-License-Identifier = "CERN-OHL-S-2.0"

[[annotations]]
path = "hardware/LICENSE"
SPDX-License-Identifier = "CERN-OHL-S-2.0"
2 changes: 1 addition & 1 deletion docs/hardware/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Constants cited below match [`include/pins.h`](../../include/pins.h).
| Interface | I2C |
| Address | `0x40` (`PCA9685_ADDRESS` in firmware). A0–A5 pads can change this |
| PWM | 12-bit, firmware `setPWMFreq(50)` |
| Important pins | As drawn: **5V** (servo-rail in), **V+** (to servo 5V), **VCC** (3V3 logic), **GND**, **SDA**, **SCL**, **PWM**. OE not drawn |
| Important pins | As drawn: **5V** (servo-rail in), **V+** (to servo 5V), **VCC** (3V3 logic), **GND**, **SDA**, **SCL**, **PWM**. **OE** (GP5) is pending, not implemented |
| Channels used | 0–4 (five servos). 5–15 unused |
| Limits | **V+** and **VCC** must stay separate. Do not jumper servo power onto logic VCC on this robot |
| Firmware note | Detection at `0x40` is a **hard fail** in the bring-up test |
Expand Down
4 changes: 2 additions & 2 deletions docs/hardware/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ Generated by **PCA9685**, not by ESP32 LEDC/MCPWM.

ESP32 GPIO is not wired to servo SIG lines.

### PCA9685 output enable (optional)
### PCA9685 output enable

| Signal | ESP32 GPIO | Notes |
| --- | --- | --- |
| OE | GPIO5 / GP5 | **Active LOW** — outputs enabled. Firmware drives **HIGH** during PCA9685 init, then **LOW** after neutral PWM is written |

Controlled only when `PCA9685_OE_WIRED = true` in [`include/pins.h`](../../include/pins.h). Default `false`: OE pin unused (Adafruit breakout pull-down keeps outputs enabled). Wire GP5 → PCA9685 **OE** and set the flag to block invalid PWM during boot and flash resets.
Not wired (`PCA9685_OE_WIRED = false` in [`include/pins.h`](../../include/pins.h)). Firmware keeps OE helpers behind that flag. Blocks invalid PWM during boot and flash resets when the net exists.

## USB

Expand Down
5 changes: 2 additions & 3 deletions docs/hardware/pinout.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Source of truth: [`include/pins.h`](../../include/pins.h). This file must match
| I2S BCLK | **GPIO2** (diagram **GP2**) | `I2S_BCLK` | MAX98357A BCLK |
| I2S LRCLK / WS | **GPIO3** (diagram **GP3**) | `I2S_LRC` | MAX98357A **LRC** |
| I2S DATA OUT | **GPIO4** (diagram **GP4**) | `I2S_DIN` | MAX98357A DIN |
| PCA9685 OE (optional) | **GPIO5** (diagram **GP5**) | `PCA9685_OE_PIN` | PCA9685 **OE** when `PCA9685_OE_WIRED` |
| PCA9685 OE | **GPIO5** (diagram **GP5**) | `PCA9685_OE_PIN` | reserved for PCA9685 **OE**; net pending (`PCA9685_OE_WIRED`) |
| Built-in WS2812 RGB | **GPIO10** | `RGB_LED_PIN` | onboard LED only |

Logic level: **3.3 V**.
Expand All @@ -30,7 +30,6 @@ Logic level: **3.3 V**.

| GPIO | Status | Notes |
| --- | --- | --- |
| GPIO5 | **Optional** | PCA9685 **OE** when wired; set `PCA9685_OE_WIRED = true` in `pins.h` |
| GPIO6 | Free | Unused |
| GPIO7 | Free | Unused |
| GPIO8 | Free | Unused |
Expand Down Expand Up @@ -60,7 +59,7 @@ Power pads (not GPIO): **5V**, **GND**, **3V3**.
| `OLED_WIDTH` / `OLED_HEIGHT` | 128 / 32 | Display |
| `SERVO_MIN_US` / `SERVO_MAX_US` | 800 / 2200 | Electrical PWM span |
| `SERVO_BOOT_SPEED_DEG_S` | 35 | Boot centering / sleep-pose rate (deg/s) |
| `PCA9685_OE_WIRED` | `false` | Set `true` after wiring GP5 → PCA9685 OE |
| `PCA9685_OE_WIRED` | `false` | pending GP5 → PCA9685 OE (not on schematic/PCB yet) |

## Pin allocation rules

Expand Down
6 changes: 3 additions & 3 deletions docs/hardware/servos.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ Verified in [`include/pins.h`](../../include/pins.h) and [`include/servos.h`](..

On every boot (and after each flash reset):

1. **Optional OE** — if `PCA9685_OE_WIRED`, GP5 disables PCA9685 outputs before I2C init.
1. **OE** — `PCA9685_OE_WIRED` is false until a tested GP5→OE board exists; firmware skips the OE pin.
2. **Early init** — PCA9685 is probed and configured immediately after `Wire.begin`, before OLED, settings, or Wi-Fi.
3. **Neutral park** — all channels receive mid-pulse PWM while OE is still disabled (if wired), then outputs enable.
3. **Neutral park** — all channels receive mid-pulse PWM (OE enable is skipped while unwired).
4. **Smooth boot moves** — `centerAllServos()` and sleep-inertia pose use `servoMoveAllSmoothTo()` at `SERVO_BOOT_SPEED_DEG_S` (35°/s), not instant snaps.

Set `PCA9685_OE_WIRED = true` in [`include/pins.h`](../../include/pins.h) after wiring GP5 → PCA9685 **OE**. OE is **active LOW** on the Adafruit breakout.
GP5 → PCA9685 **OE** is planned, not implemented (`PCA9685_OE_WIRED = false` in [`include/pins.h`](../../include/pins.h)). OE is **active LOW** on the Adafruit breakout when that net exists.

## Motion modes

Expand Down
2 changes: 1 addition & 1 deletion docs/hardware/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Build/flash: project root README (`pio run`, `pio run -t upload`, serial 115200)
| --- | --- |
| Built-in WS2812 | Green ready (GPIO10) |
| I2C init | `Wire.begin` on GPIO0/GPIO1 |
| PCA9685 | Probe `0x40` early; park neutral; optional OE on GP5 |
| PCA9685 | Probe `0x40` early; park neutral; OE skipped (`PCA9685_OE_WIRED` is false until GP5→OE is implemented) |
| OLED | Probe `0x3C`, init (optional) |
| Wi-Fi | STA connect from saved NVS credentials, or setup AP `TinyEngineer-XXXX` when unset/failed; mDNS `{hostname}.local` after STA connect |
| MAX98357A / I2S | `I2S.begin` 44.1 kHz 16-bit stereo |
Expand Down
7 changes: 3 additions & 4 deletions docs/hardware/wiring.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ On Adafruit PCA9685, header/terminal **V+** is normally the same servo rail as t
| ESP32-C3-Zero | GP2 | MAX98357A | BCLK | I2S bit clock |
| ESP32-C3-Zero | GP3 | MAX98357A | LRC | I2S word select |
| ESP32-C3-Zero | GP4 | MAX98357A | DIN | I2S data |
| ESP32-C3-Zero | GP5 | PCA9685 | OE | Optional output enable (see [interfaces.md](interfaces.md)) |
| PCA9685 | PWM | Servo | SIG | Servo PWM |
| PCA9685 | V+ | Servo | 5V | Servo power |
| PCA9685 | GND (servo side) | Servo | GND | Servo ground |
Expand Down Expand Up @@ -125,11 +124,11 @@ Firmware drives **channels 0–4** (`SERVO_CHANNELS`). The drawing does not numb

ESP32-C3-Zero, unused in the PNG:

**GP6, GP7, GP8, GP9, GP10, GP20, GP21**
**GP5, GP6, GP7, GP8, GP9, GP10, GP20, GP21**

GP5 = optional PCA9685 OE (not in PNG). GP9 = BOOT, GP10 = onboard WS2812. GP18/GP19 = native USB — wire from 5993 D−/D+ even if the base PNG omits them. See [pinout.md](pinout.md).
GP5 is reserved for PCA9685 OE (`PCA9685_OE_PIN`) but that net is **pending**: schematic and PCB do not implement or validate GP5→OE yet (`PCA9685_OE_WIRED = false`). GP9 = BOOT, GP10 = onboard WS2812. GP18/GP19 = native USB — wire from 5993 D−/D+ even if the base PNG omits them. See [pinout.md](pinout.md).

PCA9685 **OE** optional on GP5 — not drawn on base PNG. MAX98357A **GAIN** / **SD** not wired (breakout defaults). **SPK+** / **SPK-** not drawn.
PCA9685 **OE** is not wired on this board. MAX98357A **GAIN** / **SD** not wired (breakout defaults). **SPK+** / **SPK-** not drawn.

## Not on the drawing

Expand Down
31 changes: 17 additions & 14 deletions docs/pcb.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Baseline for contributing KiCad PCB designs. Goal: boards that are easy to under

Before opening a PR, run the [checklist](#checklist).

Workflow and automated validation are still evolving. A reference board and CI pipeline are in progress and will be used to validate these rules.
Workflow and automated validation: CI runs ERC, DRC, and `expected-nets.yml` via `python3 scripts/check_pcb.py` (see [testing.md](testing.md)).

KiCad sources live in [`hardware/boards/`](../hardware/README.md). Robot electrical reference (pinout, wiring, BOM) stays in [`docs/hardware/`](hardware/README.md). Firmware drivers stay in `src/hardware/`.

Expand Down Expand Up @@ -108,27 +108,33 @@ Before submitting or updating a hardware pull request:

The project should not introduce unexplained ERC or DRC errors.

Automated ERC and DRC via GitHub Actions is planned and is being tested on a reference board. It is not required yet.
Run the same checks CI uses (KiCad 10, stdlib Python — no pip packages):

```bash
python3 scripts/check_pcb.py
```

Errors fail; warnings do not. Reports go to a temp dir, or pass `--report-dir artifacts/pcb`. CI runs this on every push/PR ([`.github/workflows/ci.yml`](../.github/workflows/ci.yml)).

## expected-nets.yml

Committed checklist of **critical** net → pin memberships for human/AI review. Not a full netlist dump.

- **Path:** `hardware/boards/<board-name>/expected-nets.yml`
- **Check:** `kicad-cli sch export netlist`, then compare to this file. No `kicad-cli` → say so and skip connectivity claims ([AGENTS.md](../AGENTS.md)).
- **Check:** `python3 scripts/check_pcb.py` (exports netlist via `kicad-cli` and compares). Manual: `kicad-cli sch export netlist`, then compare to this file. No `kicad-cli` → say so and skip connectivity claims ([AGENTS.md](../AGENTS.md)).
- **Do not** infer nets from `.kicad_sch` / `.kicad_pcb` coordinates or geometry.
- **Update** when you intentionally change listed nets or pins. Keep the list to power and important buses (I2C, I2S, USB, …), not every net.

Minimal shape (pin tokens = KiCad netlist `RefDes.PinName`):
Minimal shape (pin tokens = KiCad netlist `RefDes.PinNum:PinName` — pin number plus symbol pin name, so duplicate names like multiple `GND` pins stay distinct):

```yaml
nets:
"+5V":
- ESP1.5V
- PCA1.V+
- ESP1.1:5V
- PCA1.6:V+
GND:
- ESP1.GND
- PCA1.GND
- ESP1.2:GND
- PCA1.1:GND
```

Required for new or updated boards when claiming connectivity in a PR.
Expand Down Expand Up @@ -211,8 +217,7 @@ Use this before opening or updating a PCB pull request. Details are in the secti

**Quality and docs**

- [ ] ERC run; violations reviewed
- [ ] DRC run; violations reviewed
- [ ] ERC / DRC / expected-nets: `python3 scripts/check_pcb.py` passes (or GUI ERC+DRC reviewed and nets checked if no `kicad-cli`)
- [ ] No suppressions used only to make checks pass; any intentional exception has a reason
- [ ] `expected-nets.yml` present and matches current netlist for listed nets
- [ ] Board `README.md` covers purpose, status, interfaces, assumptions, and whether this revision was manufactured and tested
Expand All @@ -233,17 +238,15 @@ Existing in-progress PRs: align where reasonably possible. Do not redesign solel

The requirements above are the current contribution baseline.

Not standardized yet (to be evaluated on the reference board):
Not standardized yet:

- automated KiCad ERC and DRC in GitHub Actions
- exact multi-board CI structure
- manufacturing jobsets
- automated manufacturing artifacts
- release and prerelease manufacturing packages
- shared KiCad libraries
- automated PCB renders or visual diffs
- manufacturer-specific output configurations

Once the reference implementation is validated, this page will be updated. Stable parts of the workflow may become required for new hardware contributions.
ERC, DRC, and `expected-nets.yml` checks run in CI via `python3 scripts/check_pcb.py`.

Keep the workflow reproducible and safe. Avoid complexity until it solves a real problem.
12 changes: 11 additions & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Tests

Host checks. Neither flashes the board. GitHub Actions on `main` and PRs runs these same commands. On-device hardware: [hardware/testing.md](hardware/testing.md).
Host checks. Neither flashes the board. GitHub Actions on `main` and PRs runs these same commands (firmware, packages, and PCB). On-device hardware: [hardware/testing.md](hardware/testing.md).

## Firmware

Expand All @@ -20,3 +20,13 @@ Node 18+. No robot.
npm test --prefix packages/tiny-engineer-cursor
npm test --prefix packages/tiny-engineer-antigravity
```

## PCB

KiCad 10 (`kicad-cli`). Stdlib Python only — no pip packages. ERC (errors), DRC (errors + schematic parity), and `expected-nets.yml` for every board under `hardware/boards/`.

```bash
python3 scripts/check_pcb.py
```

Optional: `python3 scripts/check_pcb.py main-control-board --report-dir artifacts/pcb`. Details: [pcb.md](pcb.md). On-device hardware: [hardware/testing.md](hardware/testing.md).
1 change: 1 addition & 0 deletions hardware/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.bak
6 changes: 5 additions & 1 deletion hardware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ KiCad PCB projects for Tiny Engineer.

Boards live under `boards/<board-name>/`. Prefer the same name for the directory and the KiCad project.

Contribution rules, KiCad version, what to commit, ERC/DRC, and PR expectations: [docs/pcb.md](../docs/pcb.md). Pre-PR checklist: [docs/pcb.md#checklist](../docs/pcb.md#checklist). Critical net checklist: [docs/pcb.md#expected-netsyml](../docs/pcb.md#expected-netsyml).
| Board | Role |
| --- | --- |
| [`main-control-board`](boards/main-control-board/) | Draft interconnect / carrier (header sockets for existing modules) |

Contribution rules, KiCad version, what to commit, ERC/DRC, and PR expectations: [docs/pcb.md](../docs/pcb.md). Pre-PR checklist: [docs/pcb.md#checklist](../docs/pcb.md#checklist). Critical net checklist: [docs/pcb.md#expected-netsyml](../docs/pcb.md#expected-netsyml). Local/CI checks: `python3 scripts/check_pcb.py` (KiCad 10) — [docs/testing.md](../docs/testing.md).

This tree is KiCad source only. Robot electrical reference (pinout, wiring, BOM) is [`docs/hardware/`](../docs/hardware/README.md). Firmware drivers are `src/hardware/`.

Expand Down
22 changes: 22 additions & 0 deletions hardware/boards/main-control-board/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# main-control-board

Carrier PCB that reduces jumper wiring: existing modules plug in on pin headers.

- **Purpose:** interconnect / carrier with header sockets for the existing modules
- **Status:** draft (schematic + routed layout, rev v1 / 2026-09-12)
- **KiCad:** 10
- **Interfaces:** header sockets for the Waveshare ESP32-C3-Zero, Adafruit PCA9685, MAX98357A, SSD1306 OLED, and Adafruit 5993 USB-C breakout; shared I2C (ESP32 GP0/GP1 → PCA9685 and SSD1306 OLED); point-to-point I2S (ESP32 GP2/GP3/GP4 → MAX98357A BCLK/LRC/DIN); USB D+/D− (5993 → ESP32 GP19/GP18); PCA9685 OE ← GP5; +5V, 3V3, and GND. Servo 3-pin plugs and speaker terminals stay on the modules this revision.
- **Assumptions:** nets follow `[docs/hardware/](../../../docs/hardware/README.md)` (keep servo **+5V** off logic **3V3**; common GND). Custom module symbols live in `[libraries/symbols/tiny_engineer_modules.kicad_sym](libraries/symbols/tiny_engineer_modules.kicad_sym)`; footprints in `[libraries/footprints/tiny_engineer_modules.pretty](libraries/footprints/tiny_engineer_modules.pretty)`. ESP32-C3-Zero symbol pin numbers are Waveshare **header pads** (pad 1 = 5V, numbered anticlockwise from USB), not the ESP32-C3 QFN package. Two-layer FR4 ~1.6 mm; ESP32-C3-Zero on the back, other modules on the front. OLED uses a 1×04 pin header; USB-C breakout uses a 1×04 pin socket. Netclasses: `GND`, `Power5V` (`+5V`, 1.5 mm), `Power3V3` (`+3.3V`, 0.4 mm), `USB` (`/USB_`*), `Default` (I2C/I2S and other signals).
Comment thread
jamro marked this conversation as resolved.
- **ERC / DRC:** ERC passed; DRC passed (custom rules in `main-control-board.kicad_dru`)
- **Built:** not manufactured
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- **Tested:** not tested



## Direction

First revision is sockets only. Design in KiCad 10, then fabricate, so the robot is wired on a board instead of Dupont harnesses.

Later revisions may integrate some or all of those functions onto this PCB and drop the plug-in modules.

Contribution rules: `[docs/pcb.md](../../../docs/pcb.md)`.
Loading
Loading