SMCS Kart — Software Stack Plan¶
Drafted 2026-06-11. Target milestone: first safe drive (driver-only, kart on stands first).
This plan covers the complete software stack across the Teensy 4.1 mainboard, the
Steervo ESP32, the Raspberry Pi 4, and the dashboard. Existing code in
gokart-dash/ is treated as example/prototype material: protocols and structure
are redesigned where needed, but proven pieces (kiosk deployment, arm-gate
concept, CRSF parser, bridge auto-reopen pattern) are carried forward.
1. Confirmed system facts and decisions¶
| Topic | Decision / fact |
|---|---|
| Driver input | Hori Racing Wheel Overdrive on the Teensy USB host port — wheel axis = steering, pedals = throttle/brake. Full drive-by-wire. |
| Steering | Steervo: ESP32 → PWM → Talon SRX → CIM motor via gearbox; potentiometer on ESP32 GPIO 32 for angle feedback. |
| CAN topology | 1 Mbps bus, Teensy↔ESP32 only (11-bit standard IDs). The Talon is PWM-driven (Steervo GPIO25), off-bus. (Earlier plan ran the Talon over CTRE 29-bit extended IDs on this bus; retired in favor of PWM.) |
| Braking | ESC-only (no friction brake). Binary low-brake line today; goal is proportional braking via the FarDriver serial protocol (reverse-engineered) or PWM experiments on the brake line. |
| E-stop | A physical e-stop exists independent of software. |
| RC (CRSF/ELRS) | v1 is driver-only. Protocol leaves room for remote-kill and remote-drive later. |
| Compute | Raspberry Pi 4 (dashboard, telemetry, logging — never in the motion-critical path). |
| ESC | FarDriver ND721000; no official serial docs — research + reverse-engineering track (see §7). |
Hardware action items (blockers found during review — not software)¶
- Steervo potentiometer must be powered from 3.3 V, not 5 V. The wiring doc feeds the pot from 5 V with the wiper on GPIO 32; ESP32 ADC pins are 3.3 V max and will be damaged at one end of travel.
- Talon SRX may need FRC-unlocking if it was ever connected to a RoboRIO: power it while holding its reset button ~5 s until the LED blinks green, otherwise it ignores non-FRC CAN frames.
- CAN termination: with the bus at 1 Mbps, verify exactly two 120 Ω terminations (mainboard end + far end of the bus).
2. Architecture overview¶
┌────────────────────────────────────────────┐
│ Raspberry Pi 4 — UI & telemetry (advisory) │
│ kartd (Python): UART link, WebSocket, │
│ GPS/IMU readers, blackbox logger │
│ Dashboard SPA (React kiosk, 800×480) │
└───────────────▲────────────────────────────┘
│ UART /dev/serial0 115200
│ (framed protocol, §6.1)
Hori wheel ──USB──► ┌───────────────────┴───────────────────┐
(steer/throttle/ │ Teensy 4.1 — kart-core (AUTHORITY) │
brake/buttons) │ drive state machine · pedal mapping │
│ watchdogs · fault latching │
CRSF RX ──Serial3─► │ throttle DAC · brake line · contactor│
(future kill/drive) │ hall speed · ESC serial · LED strip │
└───────┬───────────────────────┬───────┘
I2C: MCP4725│ CAN 1 Mbps │ (shared bus)
Serial1: ESC│ │
▼ ▼
FarDriver ND721000 ┌────────────────────────┐
(TPS, low-brake, │ Steervo ESP32 │
REV, speed sel, │ PWM ──────────► Talon │
serial telemetry) │ pot feedback, PID, │
│ soft limits, faults │
└────────────────────────┘
Authority hierarchy (safety invariant): the Teensy is the single source of motion authority. The Pi can request (e.g., LED color, view telemetry) but nothing the Pi sends can produce motion unless the Teensy's state machine is in DRIVE with a healthy driver-input chain. A crashed/absent Pi never affects driving. The Steervo never moves the steering motor except on fresh, valid setpoints from the Teensy.
3. Teensy firmware — kart-core¶
The safety-critical component. Replaces kart_controller.ino. Structured as a
PlatformIO project with the control logic in plain C++ modules that compile on
host for unit tests (the Arduino layer is a thin shell).
3.1 Drive state machine¶
power-on
│
▼
┌──────► SAFE ─────────────┐ all outputs off, contactor open,
│ │ ARM request │ throttle DAC at 0.5 V
│ ▼ │
│ ARMED ────────────►│ precharge then main contactor closed,
│ │ DRIVE request │ 10 s timeout back to SAFE if not driven
│ ▼ │
│ DRIVE │ pedals/steering live
│ │ fault │
│ ▼ │
└────── FAULT (latched) ◄──┘ controlled stop, then contactor open;
requires explicit driver clear + SAFE re-entry
- ARM request = deliberate driver action on the wheel (e.g., hold both shoulder paddles 1 s with brake pedal pressed and throttle released) or dashboard arm button + wheel confirmation. Never dashboard alone.
- ARMED entry sequence (precharge): the traction pack contactor has a precharge resistor (confirmed). On ARM the Teensy asserts the precharge path, dwells until the ESC bus is charged (conservative fixed dwell by default; until a bus-voltage sense crosses threshold if that line is wired), then closes the main contactor and releases precharge. A failed precharge (dwell elapses with no charge / no bus-voltage rise) aborts to SAFE with a fault. See §3.5.
- DRIVE entry conditions: wheel connected, throttle pedal at zero, hall speed = 0, and — in the normal driving configuration — Steervo heartbeat healthy and calibrated. The steering-health condition is the only thing the traction-only bench mode (§3.6) relaxes, and only with the kart on stands.
- Any fault in DRIVE triggers a controlled stop: throttle → 0, brake asserted, then contactor opens once hall pulses indicate stopped (or after a hard 3 s cap). Faults latch with a code shown on dash + LED strip color.
3.2 Control loops (all in a fixed-rate 100 Hz tick, no blocking I/O)¶
| Function | Behavior |
|---|---|
| Throttle | Hori RT axis → deadband → curve (gentle initial map) → slew-rate limiter (configurable %/s, conservative default) → MCP4725. DAC writes verified; I2C NACK = fault. |
| Brake | Hori LT axis → threshold → ESC low-brake line (v1). Proportional path added in Phase 5 (§7). Brake always overrides throttle (both pedals = brake). |
| Steering | Hori LX axis → calibrated range → angle setpoint → CAN STEER_SET at 50 Hz. Steervo runs the position loop locally. |
| Reverse / speed-profile | Wheel button selections, only honored at standstill; REV + speed-select lines driven accordingly. |
| LED strip | State signaling: SAFE=breathing white, ARMED=amber, DRIVE=green, FAULT=flashing red. Manual colors allowed only in SAFE. |
3.3 Watchdogs and failure handling¶
| Failure | Detection | Response |
|---|---|---|
| Wheel USB disconnect / stale reports | USBHost state + 100 ms report timeout in DRIVE | Fault → controlled stop |
| Steervo heartbeat loss | No STEER_STATUS for 150 ms |
Fault → controlled stop (steering holds last position, motor de-energized by Steervo's own timeout). Suppressed in traction-only bench mode (§3.6): steering is declared absent, not a fault. |
| Steervo reports fault (stall, pot out of range) | STEER_STATUS fault bits |
Fault → controlled stop. Suppressed in traction-only bench mode. |
| Teensy hang | Hardware watchdog (WDOG1), 200 ms | Reset → boots into SAFE (all outputs deterministically off in early init, as today) |
| Pi/UART loss | Informational only | Warning on LED; driving unaffected |
| ESC serial telemetry loss | Stale-data flag | Warning; driving unaffected (v1 doesn't depend on it) |
| Implausible pedal (both extremes, OOR) | Range checks | Treat as brake; fault if persistent |
3.4 Pi-facing protocol¶
Keeps the line-based human-readable command channel (great for bring-up and
kartctl), and adds a binary telemetry stream: a 20 Hz framed packet (sync
+ length + CRC16) carrying state, speeds, pedal/steering values, fault codes,
Steervo angle, ESC telemetry. Commands and telemetry are interleaved on the
same UART; frames are distinguishable from text lines by the sync byte. Full
spec lives in docs/protocols/ as the single source of truth, with packing
code generated/shared between firmware and Pi.
3.5 Contactor / precharge sequencing¶
The traction contactor has a precharge resistor, so closing it is a two-step sequence, not a single line toggle:
- Assert precharge — energize the precharge path so the resistor charges the ESC's bus capacitance through a current limit.
- Dwell — wait for the bus to charge. Default is a conservative fixed dwell (tunable; start generous, e.g. ~1–2 s). If a bus-voltage sense line is available, prefer waiting until it crosses a charged threshold, with the fixed dwell as an upper-bound timeout.
- Close main contactor, then release precharge.
A precharge that never completes (dwell elapses with the bus still low, where sensed) is a fault → abort to SAFE; never close the main contactor onto an uncharged bus. On any controlled stop / SAFE transition both the main contactor and precharge open. Hardware detail to confirm: whether precharge is a separate Teensy-driven line or a passive/automatic timer relay — code the precharge output as a configurable pin and fall back to the timed dwell if it is automatic (see §9).
3.6 Traction-only bench mode (Steervo absent)¶
A stands-only configuration that lets the rear-motor/traction path be
brought up and tested while the Steervo is unavailable (e.g., away for repair).
It is the kart-core analog of Steervo's kEnableMotorOutput gate and follows
the same discipline:
- Explicit, deliberate gate. A compile-time flag (e.g.
kTractionOnlyBenchMode) — never the default build, never shipped in the first-drive image. The normal driving configuration always keeps the full steering-health DRIVE-entry gate. - What it changes: removes only the "Steervo heartbeat healthy and
calibrated" DRIVE-entry condition and suppresses the two Steervo watchdog
faults (§3.3). No
STEER_SETenable is ever emitted. Everything else — pedal plausibility, throttle slew, precharge sequence, hall-speed-zero entry, brake override, controlled-stop on every other fault — is unchanged. - Stands-only invariant. Valid only with the driven wheels off the ground; there is no steering authority in this mode. The dash and LED strip must signal the mode loudly (it is not a driving state).
- Reverting is a one-line change + reflash, so the build that ever touches the ground is unambiguously the full-authority one.
4. Steervo firmware — steervo¶
PlatformIO ESP32 project. Replaces the hello-world steer_controller.ino.
Bring-up procedure: steering-bringup.md.
- Talon PWM driver: standard servo PWM on GPIO25 (50 Hz; 1.0 ms full
reverse / 1.5 ms neutral / 2.0 ms full forward) via the ESP32 LEDC peripheral.
The Talon must be in PWM mode (no CAN/RoboRIO owner). It self-neutralizes if
pulses stop (~100 ms) — a free hardware failsafe under ESP32 crash; we also
command neutral whenever not ACTIVE. (The earlier CTRE-CAN driver, ported from
willGuimont/CanControl, is retired
but survives as
ctre_frames.hfor reference.) - Position loop: pot on GPIO 32 (12-bit ADC) → PID to angle setpoint.
Conservative output clamp (default 40 %); a compile gate (
kEnableMotorOutput) forces neutral until supervised first motion. - Safety local to Steervo:
- Soft limits from calibration (never command past pot min/max margins).
- Pot plausibility: out-of-range or frozen-while-driving ⇒ fault, motor off.
- Stall detection: sustained max output with no pot movement ⇒ fault.
- Setpoint staleness: no
STEER_SETfor 150 ms ⇒ motor off + fault bit. - Calibration mode: guided center/end-stop calibration commanded over CAN, results stored in NVS.
CAN message set (11-bit standard IDs; full spec in protocols/can-ids.md)¶
| ID | Dir | Payload |
|---|---|---|
0x100 STEER_SET |
Teensy→ESP32 | angle setpoint (int16 centi-deg), seq, flags (enable bit) |
0x101 STEER_STATUS |
ESP32→Teensy | measured angle, output %, fault bits, seq echo — 50 Hz heartbeat |
0x102 STEER_CAL |
Teensy→ESP32 | calibration commands |
0x103 STEER_CFG |
Teensy→ESP32 | PID/limit tuning (bench only, ignored in DRIVE) |
5. Raspberry Pi — kartd + dashboard¶
5.1 kartd (Python, systemd service — evolves teensy_bridge.py)¶
One daemon owning /dev/serial0, replacing the request/response-only bridge:
- Telemetry hub: parses the binary telemetry stream; serves it to the
dashboard over WebSocket (
ws://127.0.0.1:5174/telemetry) at 20 Hz. - Command API: HTTP POST endpoints for non-critical commands (LED, view config) and requests (arm request — which the Teensy only honors with wheel confirmation). Same localhost-only model as today.
- GPS + IMU readers: carried over from the existing bridge (NEO-M9N via I2C DDC, MPU6050) — merged into the telemetry stream.
- BMS reader (battery monitoring): the pack uses a JKBMS
JK-B2A24S20P (8S–24S, 200 A, active balance) with RS485 and Bluetooth.
So pack/cell voltages, current, and temps have a dedicated source independent
of the ESC. v1 reads the BMS over RS485 (USB-RS485 adapter on the Pi)
using the community-documented JK protocol, merges pack V / current / min-max
cell / temps into the telemetry stream, and feeds the dash battery gauge.
This is non-motion-critical (same class as ESC telemetry — the Teensy never
depends on it). Bluetooth is a later alternative transport; not needed for
v1. Build as a standalone
pi/bms_probe.pyfirst (read + decode + print), then fold intokartd. - Blackbox logger: every telemetry frame + every state/fault transition to disk (SQLite or newline-JSON, rotated). This is the tuning and incident-review record; cheap to build now, invaluable on day one of drives.
- Health: systemd watchdog (
WatchdogSec), auto-restart; restart is invisible to driving by design.
5.2 Dashboard¶
The existing React kiosk stack (Cage/Chromium, 800×480, views/dock structure) is good — keep it. Changes:
useTelemetryswitches from mock sine waves to the WebSocket feed (the hook interface was designed for this).- Drive view: speed (hall-derived), drive state (SAFE/ARMED/DRIVE/FAULT — large and unambiguous), steering angle, throttle/brake bars, battery/temps once ESC telemetry lands.
- Fault surfacing: full-screen banner on FAULT with the fault code and recovery instructions; ARMED state visually loud (amber).
- System view: subsystem health (wheel, Steervo, ESC link, GPS, RC), versions, log markers.
- Lights/Map/Camera views stay as-is and remain non-critical.
6. Repository & toolchain¶
gokart-dashbecomes the software monorepo (it already contains firmware + Pi + UI), restructured:
firmware/kart-core/ # Teensy 4.1 (PlatformIO)
firmware/steervo/ # ESP32 (PlatformIO)
pi/kartd/ # Python daemon + probes
dash/ # React SPA (current src/ moves here)
tools/ # kartctl, esc_sniffer, can_tool (host CLIs)
docs/protocols/ # UART framing + CAN ID map (source of truth)
deploy/ # systemd units, kiosk scripts (as today)
- PlatformIO for both firmwares: CLI builds, pinned library versions, and host-native unit tests for the safety logic (state machine, slew limiter, CRSF/CAN parsers, plausibility checks) so safety code is tested without hardware. Ben flashes the resulting binaries.
- CI (GitHub Actions): build both firmwares, run host tests, lint/build dash.
7. ESC research track — FarDriver serial + proportional braking¶
Runs in parallel; v1 driving does not depend on it.
- Telemetry first (low risk): implement a read-only parser using the
community protocol docs
(jackhumbert/fardriver-controllers,
Endless Sphere thread) —
16-byte CRC16-protected status frames rotating through controller memory:
RPM, battery V/A, controller & motor temps, fault codes. Validate against
the bench ESC with an
esc_sniffertool (Teensy passthrough → Pi capture). - Proportional braking research: in priority order — a. Regen level via serial: the protocol docs cover writing config registers; if regen strength is settable live, map brake pedal → stepped regen levels. Test on stands. b. PWM on the low-brake line: experiment whether the ESC tolerates a duty-cycled brake input as pseudo-proportional braking (plausible but unproven — strictly a supervised bench experiment). c. Fallback: well-tuned threshold braking (two-stage: light regen via throttle-cut, full brake line past a pedal threshold).
- Any write to the ESC over serial happens only in bench mode, never in DRIVE, until the command surface is proven.
8. Phased roadmap¶
Each phase ends with a bench validation gate. Nothing that can spin a motor runs without Ben's explicit go-ahead.
Two parallel tracks¶
Steering and traction are independent below the drive state machine, so the roadmap is split into a Steering track (needs the Steervo + Talon hardware) and a Traction track (rear ESC/motor, contactor, BMS). The Steervo is away for hardware repair, so:
- Everything except the steering hardware gates can be finished now. A coding agent can complete the Traction track, the Pi/dash track, and all shared software — including writing the Steervo CAN-link code — without the Steervo present.
- Only two gates genuinely block on the Steervo's return: Phase 1's supervised steering motor test (S2) and the steering-under-load portion of full integration (I1). Those are flagged 🔒 below.
- Traction bring-up on stands runs under traction-only bench mode (§3.6), which removes the steering-health DRIVE-entry gate — stands-only, behind an explicit flag, never in a ground-driving build.
Shared foundation (done / do now)¶
| Phase | Scope | Exit gate (motors off unless noted) |
|---|---|---|
| 0. Foundations ✅ | Repo restructure, PlatformIO scaffolds, protocol spec docs, CI, host test harness | Both firmwares build in CI; protocol docs reviewed |
| K. kart-core core (shared) | Drive state machine, watchdogs, ARM chord, UART telemetry stream, LED states — host-tested; pinned SAFE on bench, no motor outputs applied | Host tests green; scaffold stays SAFE on bench; telemetry frames parse on the Pi |
Steering track (CODE now; hardware gates 🔒 wait for Steervo)¶
| Phase | Scope | Exit gate |
|---|---|---|
| 1. Steervo bench (code) ✅ | CTRE CAN driver, pot map, PID, guided calibration + NVS, fault logic | Host tests green (37 cases) |
| S1. kart-core ⇄ Steervo CAN link | FlexCAN @ 1 Mbps, Hori LX → STEER_SET at 50 Hz (ENABLE defaults off), STEER_STATUS decode + heartbeat watchdog, STEER_CAL/STEER_CFG in SAFE. Degrades gracefully when no Steervo answers (declares steering absent; in normal mode that blocks DRIVE, in traction-only mode it's ignored) |
Code builds + host-tested; on a 1 Mbps bench bus with any CAN peer the frames are well-formed (sniffer-verified). Live Steervo validation deferred to S2. |
| S2. Steervo supervised motor test 🔒 | Closes Phase 1 against the real Steervo via S1: closed-loop to setpoint, stall + staleness failsafes | ⚡🔒 When Steervo returns: supervised closed-loop + failsafe demonstration. Requires flipping STEER_SET.ENABLE and Steervo's kEnableMotorOutput — Ben's explicit go-ahead |
Traction track (Steervo not required; kart on stands)¶
Status (June 2026): T1–T4 complete — the kart drives on stands (
kart-core 0.3.2-traction). Practical notes superseded a couple of premises below: the throttle DAC is powered from the ESC's ACC+ (key) rail so it can't be verified "ESC unpowered", and the ESC speed input is a gear-cycle button. Seedocs/traction-bringup.mdandCLAUDE.md. Remaining: I2C-under-EMI hardening (HW), T4 fault drills.
| Phase | Scope | Exit gate |
|---|---|---|
| T1. Throttle DAC | Hori RT → deadband → curve → slew-rate limiter → MCP4725; I2C NACK = fault | ESC unpowered. DAC voltage tracks pedal on scope/multimeter; slew limit observed; NACK faults correctly |
| T2. ESC discrete lines + contactor/precharge | Brake-low line, REV, speed-select outputs; ARMED-entry precharge sequence (§3.5: precharge → dwell → main contactor → release) | ESC unpowered. Line states correct on multimeter; precharge/contactor sequence and timing verified; failed-precharge aborts to SAFE |
| T3. Hall speed | Hall pulse capture → speed; pulses-per-rev + wheel diameter (Q3) → mph | Speed reads correctly turning the wheel by hand; zero-speed gates DRIVE entry |
| T4. Traction-only stands bring-up ⚡ | traction-only bench mode (§3.6); ESC powered, contactor live, rear motor | ⚡ Supervised, on stands: arm (precharge→contactor), low-throttle wheel-spin, braking, fault drills (wheel pull, e-stop, implausible pedal) — no steering required |
Pi / dash / telemetry track (DO NOW — parallel)¶
| Phase | Scope | Exit gate |
|---|---|---|
| 3. Pi + dash | kartd (WebSocket telemetry, blackbox logger), dashboard real data + fault UX |
Dash shows live bench telemetry end-to-end; logger captures a session |
| B. BMS battery telemetry | JKBMS JK-B2A24S20P over RS485 → pi/bms_probe.py → kartd → dash battery gauge (Bluetooth later, §5.1) |
Pack V / current / min-max cell / temps on the dash; logged |
Integration & drive (Steervo back on the kart)¶
| Phase | Scope | Exit gate |
|---|---|---|
| I1. Full stands integration 🔒 | Both tracks together on stands: steering under load alongside traction | ⚡🔒 When Steervo returns: full arm/drive, steering under load, every fault drill (wheel pull, Steervo kill, e-stop) |
| 5. ESC serial + proportional brake | Telemetry parser, regen research (§7) | ESC telemetry on dash; braking approach selected and validated on stands |
| 6. First drive | Restore the full-authority build (traction-only bench mode off, steering-health DRIVE gate active); drive-readiness checklist, conservative limits, driver briefing | ⚡ First supervised drive |
| later | RC kill → RC drive (protocol hooks already present), camera view, maps, tuning | — |
Agent build order while the Steervo is away:
K → (T1 → T2 → T3 → T4)for traction,3 → Bfor telemetry, andS1for the steering link in code — in any interleaving. Stop at the 🔒 gates (S2, I1) and the ⚡ motor tests, which need the Steervo hardware and/or Ben at the bench. The legacy firmware cannot drive the new 1 Mbps bus (it runs 500 kbps and speaks only the old hello protocol), so any on-hardware CAN check uses the new kart-core image or thefirmware/tools/can_snifferhelper.
9. Open questions¶
Answered:
- ✅ Contactor/precharge — the contactor circuit has a precharge resistor. ARMED entry is the two-step precharge sequence in §3.5; never close the main contactor onto an uncharged bus.
- ✅ Battery monitoring — separate sensing exists: a JKBMS JK-B2A24S20P (RS485 + Bluetooth). v1 reads it over RS485 on the Pi (§5.1, Phase B); the battery gauge does not depend on ESC telemetry. Bluetooth is a later transport.
Still open (non-blocking):
- Talon device ID — what CAN device ID the Talon is configured as. Needed
for S2 (the steering motor test), discoverable on the bench with
firmware/tools/can_snifferonce the Talon is on a healthy 1 Mbps bus. Does not block the Traction track. - Hall pulses-per-rev and wheel diameter — needed to convert hall counts to mph (Phase T3 / dash). Mechanically measurable.
- ~~Precharge control topology~~ — RESOLVED (July 2026): precharge is a
Teensy-driven line on GPIO27 (HIGH = resistor energized), retrofitted
after a missing-precharge incident welded the previous contactor shut.
Firmware sequences it explicitly: precharge 2 s → resistor off → close
contactor → settle →
bus_ready(§3.5,ContactorSequencer). The resistor is thermally limited, so the sequencer enforces a 3 s hard cap and a 10 s inter-cycle cooldown. Still open: no bus-voltage sense line is wired, so charge confirmation is the fixed dwell (has_bus_sensesupport exists in the sequencer for when one is added).