Camera board (EXO-450)
Five identical boards sit in the beanie's folded cuff: three across the front and two on the slanted back rim (beanie). Each streams compressed video (MJPEG) and audio over its own Wi-Fi radio. The hub doesn't take in any video. It powers each camera, switches it on and off, and sends it a sync pulse and a slow control UART over the same 12-pin FPC part that the rest of the helmet uses.
Source: exo/pcb/camera.py (circuit as code → out/pcb/camera/camera_circuit.json), exo/pcb/camera_fp.py (land
patterns, merged into exo/pcb/footprints.json), exo/pcb/camera_place.py (placement), pcb/camera.mjs (tscircuit
build and gerber export), exo/pcb/camera_route.py (Freerouting + DRC) and exo/pcb/camera_fab.py (BOM, CPL,
gerber zip, render).
uv run python -m exo.pcb.footprints # land patterns (adds the camera's, leaves the others unchanged)
uv run python -m exo.pcb.camera # circuit -> out/pcb/camera/camera_circuit.json
uv run python -m exo.pcb.camera_place # placement -> out/pcb/camera/placement.json
node pcb/camera.mjs # tscircuit board (pads, nets, outline) -> out/pcb/camera/circuit.json
REMOTE=gb10-direct uv run python -m exo.pcb.camera_route route # Freerouting on the peer, DRC here
uv run python -m exo.pcb.camera_fab # gerbers + drill zip, JLC BOM + CPL, routed render

The board
| size | 16.45 × 18.70 mm, rectangular, 1.0 mm thick |
| stack | JLC 6-layer (the tiles' stack): top signal + GND fill, In1 GND plane, In2/In3 signal, In4 +3V3 plane, bottom signal + GND fill |
| rules | 0.1 mm track and space (0.3 mm on VSYS and the buck's switch node), 0.3/0.2 mm vias, copper ≥ 0.3 mm from the edge, pours 0.2 mm from other nets |
| via-in-pad | 65 GND / +3V3 pads have a via in the pad. Order them epoxy-filled and capped (free at JLC on 6+ layers). |
| routing | Freerouting 2.4.1: 41/41 nets, 100 %, 216 tracks, 125 vias (60 by the router, 65 in pads) |
| DRC | own check (route.json): no open nets, no shorts, no clearance or edge violations. All nets stay connected with every pour neck under 0.12 mm removed. Drill-to-drill gap ≥ 0.21 mm, via copper ≥ 0.49 mm from the edge |
| parts | 36 placed parts (20 BOM lines) + 10 test pads. About $6.1 in parts per board at JLC prices, $5.01 of it the ESP32 module |
Frame. The board is drawn as seen from the front (its outer face, towards the knit): x runs along the cuff band
(the camera's e1), y runs up the head (e2), and the origin is on the lens axis. The board spans x −11.25 … +5.20 and
y −7.70 … +11.00. Its centre is at (−3.03, +1.65), so the lens sits 3.0 mm right of the board's centre and 1.65 mm
below it.
Front (outer face): - The ESP32-S3-MINI-1 module (15.4 × 20.5 × 2.4 mm), pin-field centre at (−2.5, 0), antenna pointing +x. The antenna end hangs 4.35 mm past the right edge (module body x −10.95 … +9.55, y ±7.7). This is Espressif's preferred placement: antenna outside the base board, with no copper under or beside it. - The camera module, taped to the module's shield and centred on the lens axis (0, 0). Its 8.5 mm square body ends 0.85 mm short of the antenna keep-out. - A 3.3 mm strip above the module holds the top-port microphone (centred at (−1.7, 9.3), long side along x), the red status LED at (−8.6, 9.0) and the module's 10 µF + 100 nF.
Back (shell side): - The 24-pin camera FPC J1, under the left edge. The cable enters from the left and the opening is 0.25 mm in from the edge. - The 12-pin hub FPC J2, on the bottom edge at x = +0.8. The cable leaves downwards (−y), towards the hub. - The buck converter, the two camera LDOs, the passives and ten 1 mm test pads.
For the 3D (helmet.js / web_model.py). The cuff currently draws a 24 × 15 board with its centre at x = +3. Change it to: - board: 16.45 × 18.70 × 1.0, centre (−3.03, +1.65) in the camera frame - module can on the front: 15.4 × 20.5 × 2.4, centre (−0.70, 0), long side along x, with the antenna end (x +5.2 … +9.55) hanging off the board - camera module on the can: 8.5 × 8.5 body at (0, 0), so the lens starts 3.4 mm out from the board face (1.0 board + 2.4 can) instead of 0.8 - microphone 3.5 × 2.65 at (−1.7, 9.3); LED 1.6 × 0.8 at (−8.6, 9.0) - hub flex leaving the bottom edge at (+0.8, −7.7), on the back face - camera tail wrapping round the left edge (x = −11.25)
The board is 18.7 mm tall in the ~25 mm cuff. With the lens at cuff mid-height, it reaches 7.7 mm below the lens and 11.0 mm above it.

Parts
All parts are JLC-assemblable and carry LCSC numbers, with prices and stock checked against JLC's parts API on 2026-10-28.
| ref | part | LCSC | JLC | why |
|---|---|---|---|---|
| U1 | Espressif ESP32-S3-MINI-1-N4R2 (4 MB flash, 2 MB quad PSRAM, PCB antenna) | C3013941 | ext., $5.01, 1527 in stock | see below |
| J1 | HCTL HC-FPC-05-10-24RLTAG, 24-pin 0.5 mm, front flip, bottom contact | C5213739 | ext., $0.19, 661 | same family and drawing as the helmet's 12-pin |
| J2 | HCTL HC-FPC-05-10-12RLTAG, 12-pin (circuit.py FPC12) |
C5213732 | ext., $0.13, 19740 | the helmet's FPC part |
| U2 | TI TLV62569DBVR 2 A buck, 100 % duty mode | C141836 | ext., $0.07, 21198 | VSYS → +3V3 |
| L1 | Sunlord WPN201610H2R2MT 2.2 µH, Isat 2.15 A, 2016 | C97005 | ext., $0.04 | |
| U3 | Microne ME6211C28M5G-N, 2.8 V LDO | C53099 | ext., $0.07 | camera DOVDD/AVDD |
| U4 | TI TLV70313DBVR, 1.3 V LDO | C2868385 | ext., $0.18, 416 | camera DVDD |
| MK1 | LinkMems LMD3526T261-OA5, top-port PDM MEMS mic, 3.5 × 2.65 | C49230436 | ext., $0.31, 7396 | |
| D1 | KT-0603R red LED | C2286 | basic | |
| FB1 | Murata BLM15PD121SN1D ferrite, 0402 | C76891 | ext. | AVDD filter |
| C, R | Samsung CL05/CL10/CL21 MLCCs, UniOhm 0402WGF resistors | C1525, C52923, C19702, C45783, C25744, C25741, C25768, C25900, C11702, C25104 | basic |
Camera module (not assembled; plugged in by hand). An OV2640 2 MP, 160° wide-angle module with a 21 mm, 24-pin 0.5 mm FPC tail in the ESP32-CAM pinout. The exact listing is Probots PRO2229, "OV2640 160 Degree Wide Angle Lens 2MP Camera Module 24pin 21mm" (probots.co.in/ov2640-160-degree-wide-angle-lens-2mp-camera-module-24pin-21mm.html). It is the same commodity part sold everywhere as "OV2640 160° 21 mm for ESP32-CAM". The sensor's own JPEG engine compresses the frames: UXGA 1600×1200 at 15 fps, SVGA at 30 fps.
Why the MINI-1 and not a smaller SiP. The ESP32-S3-PICO-1 (7 × 7 mm LGA with flash, PSRAM and crystal inside) would save about 8 mm of length. It still needs its own antenna and a tuned matching network: a chip antenna, a π-network tuned on a VNA for this board and this ground plane, inside a knit hat on a head, plus its own RF certification. The MINI-1 comes certified with its antenna matched. The board takes it with the antenna hanging past the edge, which is the module's best-case layout. -N4R2 is the smallest variant with PSRAM, which holds the JPEG frame buffers. Its quad PSRAM leaves IO33–IO37 free, which the camera needs. Only IO26 goes to the PSRAM.
Why a buck and not an LDO. VSYS runs 3.4–4.4 V. At 450 mA an LDO would burn up to 0.5 W at full charge, inside a knit cuff, and at 3.4 V it would sit right in dropout. The TLV62569 runs at about 90 %. In 100 % mode it passes VSYS through at low battery: 3.4 V − 0.6 A × (≈0.1 Ω FET + 0.17 Ω inductor) = 3.24 V, inside the module's 3.0–3.6 V. VOUT = 0.6 V × (1 + 100k/22k) = 3.33 V.
Camera rails. The rails follow the two reference boards that run these modules: - DOVDD 2.8 V and AVDD 2.8 V (through FB1), as on Espressif's ESP32-S3-EYE and Seeed's XIAO ESP32S3 Sense. - DVDD 1.3 V, as on the XIAO Sense (SGM2036-1.3). The module's approval sheet (Union Image ZY-XSF3M2640B) gives 1.3 V ± 5 %.
The S3 reads the camera's 2.8 V outputs as high (VIH = 0.75 × 3.3 = 2.48 V). The SCCB lines have 4.7k pull-ups to 2.8 V. RESETB has a 10k pull-up (run) and PWDN a 10k pull-down (on).
Pinouts
Hub FPC J2. The pinout is fixed and shared with the hub designer. The connector numbering is the helmet's 12-pin land pattern.
| pin | net | camera side |
|---|---|---|
| 1, 2, 3 | VSYS | buck input (2 × 10 µF), TP1. 3 contacts × 0.4 A rating = 1.2 A |
| 4, 5, 6, 8, 12 | GND | GND (hold-down tabs too) |
| 7 | SYNC (hub → camera) | 1k → IO40 (GPIO interrupt: frame/timestamp sync) |
| 9 | CAM_TX (camera → hub) | IO41 (UART1 TX) → 1k |
| 10 | CAM_RX (hub → camera) | 1k → IO42 (UART1 RX) |
| 11 | CAM_EN (hub high = on) | buck EN (100k pull-down: no hub or hub low = off), TP3 |
The 1k series resistors keep a hub that is driving SYNC or CAM_RX from powering an unpowered camera through the S3's ESD diodes. They limit that current to < 3 mA. The hub should still drive both lines low or leave them floating while CAM_EN is low. CAM_EN needs ≥ 1.2 V (TLV62569 VIH). All logic is 3.3 V.
Camera FPC J1. The pinout is the ESP32-CAM's, the same as Espressif ESP32-S3-EYE J4 and Seeed XIAO Sense JA1:
| pin | signal | net / GPIO | pin | signal | net / GPIO |
|---|---|---|---|---|---|
| 1 | NC | – | 13 | XCLK | IO18 (LEDC, 20 MHz) |
| 2 | AGND | GND | 14 | Y8 | IO17 |
| 3 | SIO_D | IO36 (4.7k to 2V8) | 15 | DGND | GND |
| 4 | AVDD | 2.8 V via FB1 | 16 | Y7 | IO16 |
| 5 | SIO_C | IO35 (4.7k to 2V8) | 17 | PCLK | IO15 |
| 6 | RESETB | IO48 (10k to 2V8) | 18 | Y6 | IO14 |
| 7 | VSYNC | IO34 | 19 | Y2 | IO13 |
| 8 | PWDN | IO33 (10k to GND) | 20 | Y5 | IO12 |
| 9 | HREF | IO47 | 21 | Y3 | IO11 |
| 10 | DVDD | 1.3 V | 22 | Y4 | IO10 |
| 11 | DOVDD | 2.8 V | 23, 24 | NC | – |
| 12 | Y9 | IO21 |
The GPIOs run in the order their module pads come down the board's left edge. That lets the 16 camera lines drop straight through to J1 underneath without crossing. Pin 1 is at the bottom (y = −5.75).
Other GPIOs. - IO1 MIC_CLK, IO2 MIC_DATA (mic L/R tied low: left slot, data valid on the rising clock edge). - IO4 LED (high = on, 330 Ω, about 4 mA). - TXD0/RXD0, EN, IO0 and IO19/IO20 (USB D−/D+) go to test pads. - Left open: IO3, IO45 and IO46 (strapping pins; their internal pulls give JTAG over USB, 3.3 V VDD_SPI and ROM log on) and IO26 (PSRAM). - Spare: IO5–IO9, IO37–IO39.
Test pads (back, 1.0 mm, 2.2 mm pitch; one pogo jig or fine wires):
| TP1 VSYS | TP2 GND | TP3 CAM_EN | TP4 +3V3 | TP5 TXD0 |
|---|---|---|---|---|
| TP6 RXD0 | TP7 EN | TP8 IO0 | TP9 USB D− | TP10 USB D+ |
Power
These are the design figures at the 3.3 V rail, from datasheet values:
| load | mA at 3.3 V | source |
|---|---|---|
| ESP32-S3, Wi-Fi TX at 100 % duty (802.11b, 20.5 dBm, worst case) | 355 peak | MINI-1 datasheet table 6-4 |
| ESP32-S3, MJPEG streaming (802.11n MCS7 TX 286 mA bursts, RX 95 mA, CPU 240 MHz) | ~250 average | |
| OV2640 at UXGA JPEG, 140 mW at 2.8/1.3 V, drawn through LDOs from 3.3 V | ~50 | module approval sheet |
| PSRAM + flash active | ~20 | |
| mic (0.75 mA), LED (4 mA), LDO quiescent | ~5 | |
| streaming, average | ~325 |
From VSYS through the buck (η ≈ 0.9): 325 mA × 3.3 / (0.9 × 3.7 V) ≈ 320 mA at a mid battery, and about 350 mA at 3.4 V. Budget: 450 mA per camera from VSYS while streaming. That covers TX-heavy stretches and a low battery. Peaks reach about 600 mA during back-to-back 802.11b bursts. The buck (2 A), the inductor (Isat 2.15 A) and the three VSYS contacts (1.2 A) all carry that with margin.
Five cameras streaming take up to 2.25 A from the hub's VSYS, about 8 W. On the hub's 2000 mAh cell, about 7.4 Wh, the cameras alone would run ~0.9 h. The hub can run fewer cameras or lower frame rates through CAM_EN and the UART. With CAM_EN low the board draws < 2 µA (buck shutdown 0.1 µA typ. plus the 100k pull-down).
Each streaming board dissipates about 1.2 W, most of it in the ESP32's RF power amplifier. That heat sits in the cuff next to the head, so running fewer cameras or lower rates also cuts the heat.
How it streams
- The hub raises CAM_EN. The buck starts, and the S3 boots from its flash, with EN delayed by 10k/1 µF.
- The S3 joins the host's Wi-Fi network directly, not the hub. Credentials and the host address come over UART1 from the hub, which also gets them from the host.
- The esp32-camera driver runs the OV2640 in JPEG mode through the S3's LCD_CAM peripheral (DVP, 8 bit, PCLK on IO15). It writes into two frame buffers in PSRAM, in "grab latest" mode.
- Each JPEG frame goes out as an MJPEG stream: HTTP
multipart/x-mixed-replacefromesp_http_serverfor viewing, or RTP/UDP to the recorder. A 30 KB SVGA frame at 25 fps is about 6 Mb/s per camera, so five cameras use about 30 Mb/s of the 2.4 GHz channel. That is at the edge of Espressif's 20–30 Mb/s in-air figure (see hub), so the default is SVGA at 15 fps (≈ 3.6 Mb/s each). - Sync. The hub pulses SYNC on its shared frame clock. The S3 timestamps each rising edge in a GPIO interrupt
(
esp_timer_get_time) and stamps every frame and audio block with the latest SYNC count plus the µs offset from it. Video, audio and EEG then share one timeline on the host. The camera also reports each SYNC count it saw back over CAM_TX, so the hub can log any it missed. - Audio. I2S0 runs in PDM-RX mode (CLK on IO1, data on IO2) at 16 kHz/16 bit, decimated in hardware. It goes out as its own stream with the same timestamps.
- Privacy LED. IO4 is on whenever the sensor is powered (PWDN low). Firmware does not give an "off while recording" option.
Firmware notes
- Build: ESP-IDF 5.x with the
esp32-cameracomponent, targetesp32s3, PSRAM quad at 80 MHz (CONFIG_SPIRAM_MODE_QUAD), flash 4 MB QIO. camera_config_t:pin_d0..d7= 13, 11, 10, 12, 14, 16, 17, 21 (Y2…Y9)pin_xclk= 18,pin_pclk= 15,pin_vsync= 34,pin_href= 47pin_sccb_sda= 36,pin_sccb_scl= 35,pin_pwdn= 33,pin_reset= 48xclk_freq_hz= 20 MHz,pixel_format=PIXFORMAT_JPEG,frame_size=FRAMESIZE_SVGA,jpeg_quality= 12,fb_count= 2,fb_location=CAMERA_FB_IN_PSRAM,grab_mode=CAMERA_GRAB_LATEST- Fisheye: the 160° lens's image is a fisheye. Undistort on the host, after calibrating each lens once with a checkerboard.
- UART1 on IO41 (TX) and IO42 (RX), 921600 baud: control, Wi-Fi provisioning, status and SYNC echo. It is low-rate; video never goes over it.
- Strapping: don't drive IO45 high at boot. It selects 1.8 V flash power, and the module's flash is 3.3 V.
- First flash:
1. Power through TP1 (3.4–4.4 V) and TP2, and tie TP3 (CAM_EN) to TP1.
2. Hold TP8 (IO0) low and pulse TP7 (EN) low.
3. Flash over UART0 on TP5/TP6 with
esptool.py --chip esp32s3, or over the S3's own USB-Serial/JTAG on TP9/TP10 (D−/D+, no strapping needed).
After that, updates go over the air (two OTA partitions fit in 4 MB).
Assembly
- JLC assembles both sides. The front holds the module, mic, LED and two capacitors; the back holds the rest.
- Order 6 layers, 1.0 mm, with via-in-pad (epoxy filled, capped).
- The antenna hangs off the board edge. JLC's panel needs edge rails with the module side clear, or a V-cut rail past the antenna.
- Camera:
- Open J1's flip lock.
- With the board face down, slide the camera's tail into J1 from the left edge, gold fingers towards the board (J1 is bottom-contact).
- Close the lock.
- Fold the tail 180° round the left edge and stick the camera body on the module's shield with 0.2 mm double-sided PI tape, lens out, centred on the lens mark at (0, 0).
The tail runs about 3.5 mm in the connector, 0.3 mm to the edge, a ~8 mm wrap and 7 mm on the shield: 18.7 of the 21 mm. The remaining 2.3 mm just loosens the wrap. Seen through the wrap, the module's lens faces the board inside the connector. That is why J1 sits on the back and is bottom-contact: OV2640 modules carry the gold fingers on the side opposite the lens, the way the ESP32-CAM uses them. 5. Hub flex: from J2 on the back, downwards to the hub.
Land patterns
All added in exo/pcb/camera_fp.py, as new names in footprints.json; the existing entries are unchanged:
Espressif_ESP32-S3-MINI-1: datasheet v1.7 fig. 11-1 (60 × 0.4 × 0.8 at 0.85 on a 14 mm square, four 0.8 corner pads, 3 × 3 × 1.2 GND pads at 1.65), pin numbers from fig. 3-1. It matches KiCad's ESP32-S2-MINI-1, which is the same package.HCTL_FPC_24P_P0.5mm: the HCTL drawing's recommended layout (0.30 × 0.85 pads, hold-downs D = 13.27 apart and 2.91 behind). Pin 1 follows LCSC/EasyEDA's C5213739 footprint, and the same orientation as JUSHUO AFC01-S24FCC-00 on the XIAO Sense.LinkMems_LMD3526_LGA-6: datasheet sec. 8.1, top view.Ind_0806_2016,Cap_0805_2012,LED_0603_1608: IPC nominal.