80MHz SPI + immediate display_fill_rect in init caused RTC_SW_CPU_RST
boot loop. Revert to 40MHz and let hud_task handle first draw.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All 6 display pins were wrong — mapped to arbitrary GPIOs instead of
the actual Waveshare board pinout: DC=8, CS=9, SCK=10, MOSI=11, RST=12, BL=40.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tee confirmed physical wiring on Waveshare ESP32-S3 board:
- GPIO 15 = CAN TX (SN65HVD230 TXD)
- GPIO 16 = CAN RX (SN65HVD230 RXD)
- GPIO 17/18 = inter-board UART to ESP32 IO
Previous configs (GPIO 2/1, 43/44) were spec assumptions that didn't
match the actual board wiring. GPIO 43/44 are internal to PCB, not
on the header where the transceiver is connected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- gc9a01.c/h: GC9A01 240x240 round LCD SPI driver (SPI2, GPIO 9-14)
5x7 bitmap font with scaling, display_fill_rect/draw_string/draw_arc
- main.c: hud_task — "SAULT" orange header (scale=3) + battery voltage
white on black (scale=4), updates at 1 Hz from g_vesc[0].voltage_x10
- config.h: add DISP_* GPIO defines; revert 06219af UART regression —
lsusb on Orin confirms /dev/ttyACM0 = CH343 (1a86:55d3) wired to
GPIO 43/44, not native USB; UART must stay on 43/44, CAN stays on 2/1
(SN65HVD230 physical rewire to GPIO 2/1 still required for CAN to work)
- CMakeLists.txt: add gc9a01.c
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UART0 init was claiming GPIO 43/44 before TWAI could use them for CAN.
Swapping init order ensures TWAI gets GPIO 43/44 (where the SN65HVD230
transceiver is physically wired per Waveshare board design).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diagnostic proved UART protocol works (ACKs received) but CAN has zero
communication. Root cause: ESP32 connects to Orin via USB Serial/JTAG
(CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y), NOT UART0 on GPIO 43/44.
The SN65HVD230 CAN transceiver is still physically on GPIO 43/44
(original pre-bd-66hx wiring was never changed).
Fix: Put TWAI on GPIO 43/44 where the transceiver actually is.
Move unused UART0 pin config to GPIO 17/18 to avoid conflict.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three bugs blocked CAN forwarding when UART commands were received:
1. vesc_can_send_rpm had no g_twai_bus_off guard, flooding failed
twai_transmit calls during BUS_OFF/RECOVERING states.
2. Recovery only handled TWAI_STATE_BUS_OFF; RECOVERING and STOPPED
states were unhandled, leaving g_twai_bus_off=false while TWAI
was still unusable.
3. No startup delay after twai_start() — VESC not yet ready to ACK
caused immediate TEC runup to BUS_OFF at boot.
Fix: bus-off guard in send_rpm, full state machine in rx_task
(BUS_OFF→initiate, STOPPED→start, RECOVERING→wait), 200ms post-
start delay in vesc_can_init().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Define g_twai_bus_off in vesc_can.c, declare in vesc_can.h (was
referenced in main.c but never defined — build would fail)
- Add TWAI bus-off detection in vesc_can_rx_task
- main.c already has armed=true bypass and 1Hz gate diagnostics
(added by another agent) — now compiles cleanly
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
A linter reverted VESC_ID_A/B to old values 61/79. Correct IDs per
bd-wim1 protocol are 56 (left) and 68 (right).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove all OTA subsystem (gitea_ota, ota_self, uart_ota, ota_display)
and balance-bot safety checks (tilt cutoff, BAL_TILT_FAULT) so the
firmware builds without cJSON/WiFi/HTTP dependencies. Core UART protocol,
VESC CAN drive, differential steering, and PID tuning remain intact.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extends the bd-66hx serial protocol with two new Orin→ESP32 commands:
CMD_OTA_CHECK (0x10): triggers gitea_ota_check_now(), responds with
TELEM_VERSION_INFO (0x84) for Balance and IO (current + available ver).
CMD_OTA_UPDATE (0x11): uint8 target (0=balance, 1=io, 2=both) — triggers
uart_ota_trigger() for IO or ota_self_trigger() for Balance.
NACK with ERR_OTA_BUSY or ERR_OTA_NO_UPDATE on failure.
New telemetry: TELEM_OTA_STATUS (0x83, target+state+progress+err),
TELEM_VERSION_INFO (0x84, target+current[16]+available[16]).
Wires OTA stack into app_main: ota_self_health_check on boot,
gitea_ota_init + ota_display_init after peripherals ready.
CMakeLists updated with all OTA component dependencies.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds ota_display_task (5 Hz) on GC9A01 240×240 round LCD:
- Idle: orange dot badge at top-right when update available, version text
- Progress: arc sweeping 0→360° around display perimeter with phase label
- States: Downloading/Verifying/Applying/Rebooting (Balance) and
Downloading/Sending/Done (IO via UART)
- Error: red arc + "FAILED RETRY?" prompt
Display primitives (fill_rect, draw_string, draw_arc) are stubs called
from the GC9A01 SPI driver layer (separate driver bead).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Balance side (uart_ota.c): downloads io-firmware.bin from Gitea to RAM,
computes SHA256, then streams to IO over UART1 (GPIO17/18, 460800 baud)
as OTA_BEGIN/OTA_DATA/OTA_END frames with CRC8 + per-chunk ACK/retry (×3).
IO side (uart_ota_recv.c): receives frames, writes to inactive OTA partition
via esp_ota_write, verifies SHA256 on OTA_END, sets boot partition, reboots.
IO board main.c + CMakeLists.txt scaffold included.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Downloads balance-firmware.bin from Gitea release URL to inactive OTA
partition, streams SHA256 verification via mbedTLS, sets boot partition
and reboots. Auto-rollback via CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE if
ota_self_health_check() not called within 30 s of boot. Progress 0-100%
in g_ota_self_progress for display task.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds gitea_ota_check_task on Balance board: fetches Gitea releases API
every 30 min and on boot, filters by esp32-balance/ and esp32-io/ tag
prefixes, compares semver against embedded FW version, stores update info
(version string, download URL, SHA256) in g_balance_update / g_io_update.
WiFi credentials read from NVS namespace "wifi"; falls back to compile-time
DEFAULT_WIFI_SSID/PASS if NVS is empty.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces Orin↔ESP32-S3 BALANCE CAN comms (0x300-0x303 / 0x400-0x401)
with binary serial framing over CH343 USB-CDC at 460800 baud.
Protocol matches bd-wim1 (sl-perception) exactly:
Frame: [0xAA][LEN][TYPE][PAYLOAD][CRC8-SMBUS]
CRC covers LEN+TYPE+PAYLOAD, big-endian multi-byte fields.
Commands (Orin→ESP32): HEARTBEAT/DRIVE/ESTOP/ARM/PID
Telemetry (ESP32→Orin): TELEM_STATUS, TELEM_VESC_LEFT (ID 56),
TELEM_VESC_RIGHT (ID 68), ACK/NACK
VESC CAN TWAI kept for motor control; drive commands from Orin
forwarded to VESCs via SET_RPM. Hardware note: SN65HVD230
rewired from IO43/44 to IO2/IO1 to free IO43/44 for CH343.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>