Skip to content

Commit 45b76aa

Browse files
benmrossclaude
andcommitted
firmware(kart-core): traction track — Hori input, throttle DAC, gears, drive SM
Brings the kart to first powered wheel-spin on stands (SOFTWARE-STACK-PLAN §8, T1–T4): - Drive state machine: traction-only bench mode; bus_ready + contactor_ok gates; dac_ok moved to a DRIVE-entry gate (the DAC is ESC-ACC+/key powered, so it can't gate arming); key-off graceful (DAC loss only faults under throttle). - main.cpp: Hori wheel via USBHost_t36 (hardware-confirmed Xbox-One axis/button map), MCP4725 throttle with read-back verify + retries (motor EMI on I2C), brake/contactor sequencing, glitch-filtered hall speed, on/off-only LED states (PWM coupled into the hall pin), context-sensitive DRIVE button, paddle-shift gears (ESC high-speed line is a gear-cycle button, tracked open-loop), WDOG1. - New host-tested modules: arm_chord, hall_speed, contactor_seq (+ test_traction); drive_state tests updated. 50 native cases pass. - Bench/diagnostic serial commands: rich STATUS gate bits + DAC counters, WHEELRAW, I2C scan, DACREAD, DACSET, GEAR. - gen_arduino_sketch.sh + generated arduino/kart_core/ (flash from the IDE; CLI upload doesn't re-power the wheel); dac_test bench tool. - Docs: traction-bringup.md (operations + hard-won lessons), uart-protocol command/gesture updates, consolidated SOFTWARE-STACK-PLAN. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6f582cf commit 45b76aa

31 files changed

Lines changed: 4179 additions & 102 deletions

docs/SOFTWARE-STACK-PLAN.md

Lines changed: 155 additions & 18 deletions
Large diffs are not rendered by default.

docs/protocols/uart-protocol.md

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,51 @@ driver confirmation on the wheel.
3939
| `FAULT_CLEAR` | `OK` / `ERR FAULT_ACTIVE` | Clears a *latched* fault only after its cause is gone and the kart is stopped. |
4040
| `LED <r> <g> <b>` | `OK LED …` | SAFE state only; otherwise LEDs signal drive state. |
4141
| `STEER_CAL <enter\|center\|left\|right\|save\|abort>` | `OK …` | Forwarded to Steervo (`0x102`), SAFE only. |
42-
| `CFG <name> <value>` / `CFG?` | `OK …` | Bench tuning (slew rates, pedal curve), SAFE only, rejected when armed. |
43-
| `ESC_READ [n]` / `ESC_WRITE <hex>` | `OK …` | ESC serial passthrough for the research track. `ESC_WRITE` is bench-mode only (SAFE + explicit `BENCH on`). |
44-
| `BENCH <on\|off>` | `OK BENCH …` | Enables bench-only commands; refused if hall speed ≠ 0 or state ≠ SAFE. |
45-
| `VERSION` | `OK VERSION kart-core <semver> proto=1` | |
42+
| `CFG <name> <value>` / `CFG?` | `OK …` | Bench tuning, SAFE only. Implemented keys: `axis_thr`, `axis_brk`, `axis_steer` (USB-host axis indices), `ped_released`, `ped_pressed` (pedal raw-value calibration). |
43+
| `WHEELRAW` | `OK WHEELRAW enum=… type=… buttons=0x… a0=… … a15=…` | Dumps the live USB-host wheel state (all axes + button mask + joystick type). Use it to discover the Hori pedal axis indices and ranges through `USBHost_t36`, then set them with `CFG`. Read-only, any state. |
44+
| `I2C` | `OK I2C found 0x.. … dac@0x60` | Scans the MCP4725 I2C bus and lists ACKing addresses. Read-only. Empty = DAC unpowered (key off) or not present. |
45+
| `DACREAD` | `OK DACREAD dacval=…/4095 pd=… settings=0x…` | Reads back the MCP4725 register + power-down bits — verifies the DAC actually stored what was written (catches silent I2C corruption). Needs the DAC powered (key on). |
46+
| `DACSET <pct>` | `OK DACSET …% for 10s` | **SAFE only.** Holds a fixed throttle DAC voltage for ~10 s for multimeter/scope verification (contactor open → no motion). Auto-clears; refused outside SAFE. |
47+
| `GEAR <low\|med\|high>` | `OK GEAR …` | Resyncs the firmware's open-loop gear model to what the FarDriver app shows (does **not** pulse the line). Use if the LED gear color drifts from the ESC. |
48+
| `ESC_READ [n]` / `ESC_WRITE <hex>` | `OK …` | ESC serial passthrough for the research track. `ESC_WRITE` is bench-mode only (SAFE + explicit `BENCH on`). *(planned; not yet implemented)* |
49+
| `BENCH <on\|off>` | `OK BENCH …` | Enables bench-only commands; refused if hall speed ≠ 0 or state ≠ SAFE. *(planned; not yet implemented)* |
50+
| `VERSION` | `OK VERSION kart-core <semver> proto=1` | Current: `0.3.2-traction`. |
51+
52+
`STATUS` (rich, used heavily for bring-up diagnostics) reports:
53+
`state fault bench wheel thr brk hall hz10 contactor rev speed dac dacok dacfail plaus busrdy vstop thr0 wheelok`
54+
— where `dac/busrdy/thr0/wheelok/plaus/vstop` are the live DRIVE-entry/health gate
55+
bits (1 = satisfied) and `dacok/dacfail` are cumulative DAC write success/fail
56+
counts (watch the deltas to see I2C health under motor load).
4657

4758
Legacy commands (`THROTTLE`, `BRAKE`, `CONTACTOR`, `SPEED`, `OUTPUT`, …) exist
4859
in the legacy firmware for bring-up via `tools/kartctl.py`; kart-core does
4960
**not** accept direct actuation from the Pi — pedals and the state machine are
5061
the only motion path.
5162

63+
### Operator gestures (wheel)
64+
65+
Arming and drive-state changes happen **on the wheel**, never from the Pi alone
66+
(spec §3.1). The Pi `ARM_REQ` is advisory; it does not arm.
67+
68+
Button bits below are the **hardware-confirmed** Hori-via-`USBHost_t36` Xbox-One
69+
mapping (not the Linux-js numbers): paddles = bits 12 (left) / 13 (right),
70+
DRIVE button = bit 6.
71+
72+
| Gesture | Action |
73+
|---|---|
74+
| Both shift paddles (bits 12+13) held + brake pressed + throttle released, **1 s** | ARM chord: SAFE → ARMED |
75+
| **DRIVE button (bit 6)** — context-sensitive: in ARMED → DRIVE (needs throttle 0, `bus_ready`, `dac_ok`); in DRIVE → disarm; in FAULT → clear | the whole flow from one button |
76+
| Pi `DISARM`/`SAFE` | Controlled stop → SAFE (also available any time) |
77+
| Pi `FAULT_CLEAR` | Clear a latched FAULT (at rest, cause resolved) |
78+
| **Right paddle (bit 13)** while armed/driving | Upshift — 1 gear-cycle pulse on the high-speed line (clamps at HIGH) |
79+
| **Left paddle (bit 12)** while armed/driving | Downshift — 2 pulses (down one in the 3-gear wrap; only above LOW) |
80+
| **X button (bit 2)**, at standstill | Toggle reverse intent (direction itself is set in the FarDriver app) |
81+
82+
LED states: SAFE = **solid** magenta (traction-only bench) / white (normal);
83+
ARMED = amber; DRIVE = gear color (**green=LOW, cyan=MED, blue=HIGH**);
84+
STOPPING = flashing amber; FAULT = flashing red. LEDs are driven fully on/off
85+
only — never PWM, because the PWM coupled into the hall input on pin 2.
86+
5287
---
5388

5489
## 2. Binary telemetry channel (Teensy → Pi)
@@ -124,3 +159,4 @@ firmware emits both during a transition window.
124159
| 5 | `DAC_ERROR` | MCP4725 I2C write NACK | Hardware check, clear |
125160
| 6 | `ARMED_TIMEOUT` | ARMED with no DRIVE entry for 10 s | Informational; auto-return to SAFE (not latched) |
126161
| 7 | `INTERNAL_WDT` | Loop overrun detected pre-watchdog-reset | Investigate before clearing |
162+
| 8 | `CONTACTOR_FAULT` | Contactor/bus sequencer fault (bus-voltage sense never confirmed charge). Dormant today: precharge is an always-on external resistor and no bus-sense line is wired | Inspect contactor + bus, clear, re-arm |

docs/traction-bringup.md

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# Traction Track Bring-up (kart-core, on stands)
2+
3+
Step-by-step for the traction phases T1–T4 (SOFTWARE-STACK-PLAN.md §8). This is
4+
the `kart-core` firmware in `firmware/kart-core`, built with
5+
**`KART_TRACTION_ONLY_BENCH = 1`** (config.h) because the Steervo is away — so
6+
there is **no steering authority**. Valid only with the driven wheels off the
7+
ground. The LED strip is solid **magenta** in SAFE to signal this mode loudly.
8+
9+
> Nothing here spins the motor until **T4**, which is supervised, with the ESC
10+
> powered and Ben at the bench. T1–T3 run with the **ESC unpowered**.
11+
12+
## Build & flash
13+
14+
```bash
15+
pio run -d firmware/kart-core -e teensy41 # -> .pio/build/teensy41/firmware.hex
16+
pio run -d firmware/kart-core -e teensy41 -t upload # Ben flashes
17+
pio test -d firmware/kart-core -e native # host logic tests (no hardware)
18+
```
19+
20+
To return to the full-authority build later, set `KART_TRACTION_ONLY_BENCH 0`
21+
in `firmware/kart-core/src/config.h` and reflash.
22+
23+
## Talk to it
24+
25+
Over USB serial (`/dev/ttyACM*`) or the Pi UART (`/dev/serial0`, 115200):
26+
27+
```
28+
PING -> OK PONG
29+
VERSION -> OK VERSION kart-core 0.3.2-traction proto=1
30+
STATUS -> rich gate/health snapshot (see below)
31+
WHEELRAW -> every wheel axis + button mask (axis/button discovery)
32+
I2C -> scan the DAC bus (lists ACKing addrs; empty = DAC unpowered)
33+
DACREAD -> read back the MCP4725 (verify it stored what we wrote)
34+
DACSET <pct> -> SAFE-only: hold a fixed throttle voltage for metering (10 s)
35+
GEAR <low|med|high> -> resync the open-loop gear model to the FarDriver app
36+
DISARM / FAULT_CLEAR -> stop / clear a latched fault from the Pi side
37+
```
38+
39+
`STATUS` fields used constantly for diagnosis:
40+
`state fault bench wheel thr brk hall hz10 contactor rev speed dac dacok dacfail plaus busrdy vstop thr0 wheelok`.
41+
The gate bits (`dac busrdy thr0 vstop plaus wheelok`, 1 = satisfied) tell you
42+
*why* arming/DRIVE is blocked; `dacok/dacfail` are cumulative DAC-write counters
43+
(watch the deltas to see I2C health under motor load).
44+
45+
> ⚠️ **Hardware reality found during bring-up (corrects the ESC-unpowered
46+
> premise of T1–T3 below):** the throttle DAC is powered from the ESC's **ACC+
47+
> rail, which is gated by the key**. So the DAC only works with the ESC keyed
48+
> on — you *cannot* verify the throttle DAC with the ESC off. `dac_ok` is
49+
> therefore a **DRIVE-entry gate**, not an arm gate, and the real power-up order
50+
> is **arm (contactor closes) → key on (DAC powers) → DRIVE → throttle.** See
51+
> "Bench operations & hard-won lessons" at the bottom — read that first.
52+
53+
## T0 — confirm the wheel axes (do this first)
54+
55+
The pedal axis indices/ranges in `steering-wheel.md` were measured on the Pi's
56+
Linux `js` API. Through the Teensy's `USBHost_t36` Xbox decoder they **may
57+
differ** (triggers are sometimes `0..1023`, not `±32767`). Confirm before
58+
trusting the throttle:
59+
60+
1. Plug the wheel into the mainboard USB-A. Watch for
61+
`INFO WHEEL_CONNECTED vid=0x0F0D pid=0x0152 type=…`.
62+
2. Run `WHEELRAW` repeatedly while pressing **only the throttle**, then **only
63+
the brake**, then turning the wheel. Note which `aN` moves for each and its
64+
released vs. fully-pressed values.
65+
3. If they differ from the defaults (`axis_thr=5`, `axis_brk=4`, `axis_steer=0`,
66+
released `-32767`, pressed `+32767`), set them live:
67+
```
68+
CFG axis_thr 5
69+
CFG ped_released -32767
70+
CFG ped_pressed 32767
71+
```
72+
(RAM only — once confirmed, bake the values into `config.h` so they persist.)
73+
74+
## T1 — throttle DAC (ESC unpowered)
75+
76+
Goal: DAC output (ESC connector pin 27, TPS) tracks the throttle pedal,
77+
0.5 V released → ~4.3 V full, with a visible slew-rate ramp.
78+
79+
1. Put a scope/DMM on the throttle DAC line (relative to throttle ground).
80+
At boot it should sit at **~0.5 V** (idle floor) in SAFE.
81+
2. **Arm:** hold **both paddles (LB+RB)** + press the **brake** + leave throttle
82+
released, for **1 second**. LED → amber (ARMED), contactor closes after the
83+
settle dwell. (ESC unpowered, so closing it is harmless.)
84+
3. **Drive:** press **Menu/Start** with the throttle at zero. LED → green
85+
(DRIVE).
86+
4. Press the **throttle**: the DAC voltage should rise smoothly (slew-limited,
87+
~25 %/s) toward 4.3 V and snap back to 0.5 V instantly on lift.
88+
5. Press the **brake** while on throttle → DAC drops to idle (brake overrides).
89+
6. **Stop:** press **View/Select** (or send `DISARM`). LED → white-ish (back to
90+
SAFE in normal mode; magenta here in bench mode).
91+
92+
Fault check: unplug the wheel during DRIVE → controlled stop → latched
93+
`WHEEL_LOST` (LED flashing red). Reconnect, `FAULT_CLEAR`, re-arm.
94+
95+
## T2 — ESC discrete lines + contactor (ESC unpowered)
96+
97+
On a DMM at the ESC connector, verify the MOSFET ground lines assert correctly:
98+
99+
- **Contactor (pin 32):** open in SAFE; closes on ARM after the settle dwell;
100+
opens again on SAFE/FAULT. Precharge is the **always-on external 100 Ω BMS
101+
resistor** — not Teensy-controlled — so the bus is already charged before you
102+
ever arm.
103+
- **Brake-low (ESC pin 21):** asserts when the brake pedal passes threshold in
104+
DRIVE, and throughout a controlled stop.
105+
- **Reverse:** direction is set in the FarDriver app (motor direction); the
106+
firmware leaves the REV line at its natural sense (X button toggles it).
107+
- **Gear (ESC high-speed line):** this ESC's high-speed input is a momentary
108+
gear-up-cycle button (1→2→3→1 per pulse). Right paddle = upshift (1 pulse,
109+
clamps at HIGH), left paddle = downshift (2 pulses = down one in the wrap,
110+
only above LOW). Gear is tracked open-loop and shown on the LED
111+
(green/cyan/blue); resync with `GEAR <low|med|high>` if it drifts from the app.
112+
113+
## T3 — hall speed
114+
115+
Spin the rear wheel **by hand**. `STATUS` (`hall=`, `hz10=`) and the telemetry
116+
stream should show pulses and a non-zero frequency. While "moving," DRIVE entry
117+
is blocked (zero-speed is a DRIVE-entry gate) and a controlled stop waits for
118+
zero speed. After ~300 ms with no pulses it reads stopped again.
119+
120+
## T4 — supervised wheel spin ⚡ (Ben + ESC powered, on stands)
121+
122+
Only after T1–T3 pass. ESC powered, contactor live, rear wheel free. Arm →
123+
low-throttle wheel-spin → brake → fault drills (wheel pull, e-stop, implausible
124+
pedal). This is the §8 ⚡ gate and needs Ben's explicit go-ahead.
125+
126+
---
127+
128+
# Bench operations & hard-won lessons
129+
130+
Everything below was learned getting the kart to actually drive. **Read this
131+
before bench work** — much of it is non-obvious and cost real debugging time.
132+
133+
## Driver control scheme (current, hardware-confirmed)
134+
135+
The Hori enumerates through `USBHost_t36` as **Xbox One (joystickType 3)**, and
136+
its button/axis map there is **not** the Linux-`js` map in `steering-wheel.md`.
137+
Confirmed mapping (baked into `src/config.h`):
138+
139+
| Control | Where | Notes |
140+
|---|---|---|
141+
| Throttle | axis **4**, `0…1023` | (not axis 5 / ±32767) |
142+
| Brake | axis **3**, `0…1023` | |
143+
| Steering | axis **0**, `±32767` | unused in traction build |
144+
| Left paddle | button bit **12** | downshift |
145+
| Right paddle | button bit **13** | upshift |
146+
| DRIVE button | button bit **6** | context-sensitive (below) |
147+
| X button | bit **2** | reverse-intent toggle |
148+
149+
- **Arm:** hold **both paddles + brake**, throttle released, ~1 s → amber.
150+
- **DRIVE button (bit 6) is the whole flow:** ARMED→DRIVE, DRIVE→disarm,
151+
FAULT→clear. (DISARM/FAULT_CLEAR also over UART.)
152+
- **Gears (paddles, while armed/driving):** right = upshift, left = downshift.
153+
LED in DRIVE shows the gear: **green=LOW, cyan=MED, blue=HIGH**.
154+
155+
## The power-up / drive / shutdown sequence (order matters)
156+
157+
```
158+
SAFE → arm chord → ARMED (contactor closes) → turn ESC KEY ON (DAC powers,
159+
dac=1) → DRIVE button → DRIVE → throttle.
160+
Shutdown: lift throttle → DRIVE button (disarm) → SAFE → THEN key off.
161+
```
162+
163+
- **Key after contactor** (hardware rule): never key the ESC before the
164+
contactor is closed. Arming closes the contactor; key on during ARMED.
165+
- DRIVE needs `dac=1` (key on). If you press DRIVE with `dac=0` it just sits in
166+
ARMED and times out (30 s). `STATUS` shows the gate.
167+
- **Disarm before keying off.** Keying off while driving cuts DAC power; the
168+
firmware only faults on that if throttle is pressed (it's graceful at a stop),
169+
but disarm-first is the clean path.
170+
171+
## Flashing workflow (important)
172+
173+
- **Flash from the Arduino IDE**, not `pio … -t upload`. The CLI's auto-reboot
174+
into the bootloader fails on this rig (the program button is hard to reach in
175+
the enclosure), *and* — crucially — the wheel only re-enumerates on the
176+
Teensy USB host after the IDE flow power-cycles it. A CLI soft-reboot leaves
177+
the wheel dark.
178+
- kart-core is a PlatformIO project; the IDE needs a flat sketch. Run
179+
**`firmware/kart-core/gen_arduino_sketch.sh`** to (re)generate
180+
`arduino/kart_core/` from the canonical `src/` + `lib/` + `firmware/common`
181+
(it vendors WDT_T4 and rewrites includes). Open `arduino/kart_core/kart_core.ino`,
182+
select Teensy 4.1, USB Type Serial, upload. **Never hand-edit `arduino/`.**
183+
- Confirm the flash took with `VERSION` (bump `kVersion` in `main.cpp` each
184+
change so you can tell). The Teensy is board-powered, so unplugging the USB
185+
data cable does **not** reboot it.
186+
187+
## Monitoring (how to watch what's happening)
188+
189+
There's no installed pyserial; use uv. Talk to `/dev/ttyACM0` @ 115200:
190+
191+
```bash
192+
uv run --with pyserial python - <<'PY'
193+
import serial,time
194+
s=serial.Serial("/dev/ttyACM0",115200,timeout=0.3); time.sleep(0.2)
195+
s.write(b"STATUS\n"); time.sleep(0.3); print(s.read(s.in_waiting).decode())
196+
PY
197+
```
198+
199+
For live tests, run a **background poller** that prints `STATUS`/`WHEELRAW`
200+
deltas (state, `thr`, `dac`, `dacfail`, `hall`, buttons) — unbuffered
201+
(`python -u`, `flush=True`) writing to a log you `tail`. This is how every drive
202+
test was debugged (which gate blocked, whether DAC writes failed under motor
203+
load, which button bit fired). Watch **`dacfail` deltas during the motor spin**
204+
to gauge I2C noise. Gotchas: the 20 Hz binary telemetry goes to `Serial2`
205+
(Pi UART) **not** USB, so over USB you only get command replies + `INFO` lines;
206+
and never `pkill -f` a pattern that also matches the launching shell (it
207+
self-kills) — use `fuser -k /dev/ttyACM0` to free the port.
208+
209+
## Hardware quirks & gotchas discovered
210+
211+
- **Throttle DAC (MCP4725, header J3, addr 0x60) is powered by ESC ACC+**
212+
only alive with the key on. An empty `I2C` scan = key off, not a dead DAC.
213+
- **A missing series resistor (R10, 1 kΩ) in the DAC→ESC throttle path** made
214+
the DAC read fine at VOUT but deliver 0 V to the ESC — looked exactly like a
215+
dead DAC. If the ESC's throttle reads 0 in the app, check that path/`R10`.
216+
- **Motor EMI corrupts the DAC's I2C** once the wheel spins → DAC_ERROR. Each
217+
write is now read-back-verified (`writeThrottleRaw`) and only faults after
218+
`kDacFailMs` of continuous failure. Real fix is HW: stronger I2C pull-ups
219+
(2.2 k), routing SDA/SCL away from motor phases, DAC VCC decoupling. The
220+
Teensy I2C pins are **3.3 V only (not 5 V tolerant)** — pull-ups must not hard
221+
pull to 5 V.
222+
- **Hall input (pin 2) picks up noise:** the LED strip's 24 V **PWM** coupled in
223+
→ false motion → blocked arming. Fixed by driving the LEDs **on/off only,
224+
never PWM**, plus an ISR glitch filter (`kHallMinIntervalUs`). With the ESC
225+
keyed on, the hall line is also noisy (`vstop` flickers) — harmless for DRIVE
226+
entry, but it can block FAULT_CLEAR/arming until the wheels truly stop.
227+
- **ESC high-speed line is a momentary gear-cycle button** (1→2→3→1 per
228+
grounding pulse), not a level select. Firmware pulses it (up=1, down=2) and
229+
tracks gear **open-loop** — resync with `GEAR` if it drifts.
230+
- **Motor direction + commutation are ESC-side:** a buzzing/vibrating motor =
231+
hall/phase mapping → fix with the **FarDriver app's motor self-learn**.
232+
Forward/reverse direction is also set in the FarDriver app.
233+
- **`KART_TRACTION_ONLY_BENCH = 1`** in `config.h` for all of the above
234+
(stands-only, no steering). Set to `0` for any ground-driving build.

firmware/README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,13 @@ CI builds both images and runs all native tests on every push/PR.
2727
I/O shell. Keep it that way: new logic goes in `lib/` with tests.
2828
- **Wire formats live in `firmware/common`** and must match
2929
`docs/protocols/`. Never hand-roll packing in a sketch.
30-
- **Phase 0 gates:** the kart-core shell pins itself in SAFE (stub inputs),
31-
and steervo compiles with `kEnableMotorOutput = false` so even a fully
32-
active controller commands zero demand. These gates come off only at the
33-
supervised bench tests in Phases 1–2.
30+
- **kart-core traction track (T1–T4):** real Hori-wheel input, throttle DAC,
31+
ESC discrete lines, contactor sequencing, and hall speed are implemented and
32+
host-tested. The image ships with `KART_TRACTION_ONLY_BENCH = 1`
33+
(`src/config.h`) — stands-only, **no steering authority**, LED is solid magenta
34+
— because the Steervo is away. Set it to `0` for any ground-driving build.
35+
See [`docs/traction-bringup.md`](../docs/traction-bringup.md). steervo still
36+
compiles with `kEnableMotorOutput = false`. The motor only spins at the
37+
supervised ⚡ bench gate (T4), never from CI or by default.
3438
- Flashing is always done by a human (Arduino IDE/`pio run -t upload` from a
3539
workstation), never from CI or the Pi.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# kart_core — Arduino IDE sketch (GENERATED)
2+
3+
Do **not** edit files here. This folder is assembled from the canonical
4+
PlatformIO sources by `firmware/kart-core/gen_arduino_sketch.sh`. Edit the
5+
originals (`src/`, `lib/kartcore/`, `firmware/common/`) and re-run that script.
6+
7+
Open `kart_core.ino` in the Arduino IDE, select **Teensy 4.1**, and upload.
8+
USBHost_t36 and Wire ship with Teensyduino; WDT_T4 is vendored here.

0 commit comments

Comments
 (0)