From 35b7074e8143c5422d1ddb58e51c260254e5171f Mon Sep 17 00:00:00 2001 From: Thaddeus Hughes Date: Thu, 12 Mar 2026 08:38:39 -0500 Subject: [PATCH] docs & sdkconfig --- CLAUDE.md | 312 ++++++--------------------------------------- README.md | 280 +++++++++++++++++++++++++++++++++++++++- TODO.md | 106 +++++++-------- main/control_fsm.c | 103 ++++++--------- sdkconfig | 2 +- sdkconfig.defaults | 6 + 6 files changed, 417 insertions(+), 392 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ca8a404..0e1c005 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,277 +1,55 @@ # SC-F001 Firmware — CLAUDE.md -## Overview - -The SC-F001 is a **solar-powered automated crop harvesting robot** built on the ESP32. It drives a carriage horizontally via a drive motor and lifts/lowers a cutting head via a jack motor, with an auxiliary "fluffer" motor always running during operation. The firmware handles motor sequencing, safety interlocks, remote control, data logging, and a WiFi web interface. - -**Primary operational cycle:** Idle → Move Start Delay → Jack Up → Drive → Jack Down → Idle +See `README.md` for full project documentation (hardware, architecture, protocols, algorithms). --- -## Hardware Platform +## Workflow -**MCU:** ESP32 (Xtensa dual-core), IDF framework +- **Minimize shell commands.** Every Bash call requires user approval. Prefer Read/Edit/Write/Glob/Grep tools. Only use Bash when a shell command is genuinely needed (e.g., `idf.py build`, git operations). +- **Webpage build step:** After editing `webpage.html`, run `webpage_compile.py` to regenerate `webpage_gzip.h` before building. +- **Don't touch git.** +--- -**GPIO Map:** -| GPIO | Function | -|------|----------| -| 13 | Button interrupt (active low, pull-up) — also EXT0 wakeup | -| 14 | Jack position sensor / encoder | -| 16 | Drive encoder | -| 19 | Aux sensor 2 (reserved) | -| 21/22 | I2C SDA/SCL (400kHz) → TCA9555 I/O expander | -| 25 | 433MHz RF receiver (RMT input) | -| 26 | Solar charger bulk enable (RTC GPIO, holds across deep sleep) | -| 27 | Safety sensor (active low) | -| 32/33 | External 32.768 kHz RTC crystal (standard watch crystal, 2¹⁵ Hz) | -| 36 (VP) | ADC: drive current sense | -| 39 (VN) | ADC: battery voltage | -| 34 | ADC: jack current sense | -| 35 | ADC: aux current sense | +## sdkconfig Management -**TCA9555 (I2C at 0x20):** -- Port 0 (input): 2 physical buttons + 2 additional inputs -- Port 1 (output): 3× H-bridge relay pairs (DRIVE, JACK, AUX) + LEDs +**Two files, different roles:** +- `sdkconfig.defaults` — checked into git. Contains only intentional project overrides with comments explaining why. Applied by `idf.py reconfigure` on top of IDF defaults. +- `sdkconfig` — generated/modified by `idf.py menuconfig` or `reconfigure`. Contains every resolved setting. Also checked in for reproducibility, but treat `sdkconfig.defaults` as the source of truth for project-specific choices. -**Motor / Bridge Specs:** -- `BRIDGE_DRIVE` — 100A max, ACS37220 sense chip (13.2 mV/A, inverted polarity) -- `BRIDGE_JACK` — 30A max, ACS37042 sense chip (44 mV/A) -- `BRIDGE_AUX` — 30A max, ACS37042 sense chip (44 mV/A) +**Rules:** +- When changing a setting, add it to `sdkconfig.defaults` with a comment, then also apply it to `sdkconfig` so the next build picks it up without requiring `idf.py reconfigure`. +- Never hand-edit `sdkconfig` without also updating `sdkconfig.defaults` for the same setting — otherwise the change will be lost on the next `reconfigure`. +- Keep `sdkconfig.defaults` small and well-commented. Don't dump the full config into it. + +**Current project-specific overrides (sdkconfig.defaults):** +| Setting | Value | Why | +|---------|-------|-----| +| `CONFIG_RTC_CLK_SRC_EXT_CRYS` | y | Use external 32kHz crystal for accurate RTC | +| `CONFIG_ESP32_RTC_EXT_CRYST_ADDIT_CURRENT_V2` | y | Drive high-ESR crystal during startup | +| `CONFIG_ESP_SYSTEM_RTC_EXT_XTAL_BOOTSTRAP_CYCLES` | 500 | Extra bootstrap for slow-starting crystal | +| `CONFIG_RTC_XTAL_CAL_RETRY` | 3 | More calibration attempts before RC fallback | +| `CONFIG_ESP_TASK_WDT_PANIC` | y | WDT timeout → panic → reboot (feeds OTA rollback counter) | + +**Already correct at IDF defaults (verified, no override needed):** +| Setting | Value | Status | +|---------|-------|--------| +| `CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY` | y | Stack overflow detection via canary (method 2) | +| `CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT` | y | Print backtrace then reboot on panic | +| `CONFIG_BROWNOUT_DET_LVL_SEL_0` | y | ~2.43V brownout on ESP32 3.3V rail (appropriate — battery low-V is handled by `LOW_PROTECTION_V` in FSM) | +| `CONFIG_PARTITION_TABLE_CUSTOM` | y | Custom partitions.csv with ota_0 + ota_1 | --- -## Software Architecture +## Conventions -``` -app_main() -├── rtc_xtal_init() RTC crystal + EXT0 wakeup + sleep wakeup check -├── i2c_init() TCA9555 init (relays off, LEDs off) -├── adc_init() ADC1 calibration (12dB attenuation, line-fit) -├── storage_init() Flash params + circular log buffer -├── solar_run_fsm() (called in main loop too) -├── uart_init() Serial JSON API task -├── rf_433_init() 433MHz RMT receiver task -├── bt_hid_init() BLE HID host scanner task -├── fsm_init() Control FSM task (priority 10, 20ms tick) -└── webserver_init() WiFi softAP + HTTP + mDNS + DNS - -Main loop (50ms): - i2c_poll_buttons() - fsm_request() based on button events - solar_run_fsm() - driveLEDs() status animation - rtc_check_shutdown_timer() → deep sleep on inactivity (180s) -``` - -**Task Priorities:** -- FSM control task: priority 10 (real-time) -- All others: default priority - ---- - -## Key Files - -| File | Purpose | -|------|---------| -| `main.c` | Entry point, 50ms main loop, factory reset, LED animation | -| `control_fsm.c/h` | State machine, relay control, current monitoring, calibration | -| `power_mgmt.c/h` | ADC reading, e-fuse thermal algorithm, battery voltage | -| `sensors.c/h` | GPIO ISR-based sensor debouncing, encoder counters | -| `i2c.c/h` | TCA9555 relay/LED/button control | -| `storage.c/h` | 47-param NVM table + circular binary log buffer | -| `comms.c/h` | Unified GET/POST JSON API (shared by HTTP and UART) | -| `webserver.c/h` | WiFi softAP, HTTP server, embedded gzip webpage | -| `uart_comms.c/h` | Serial JSON interface (115200 8N1) | -| `rf_433.c/h` | 433MHz OOK receiver, keycode learn/match | -| `bt_hid.c/h` | BLE HID host, media remote button mapping | -| `rtc.c/h` | Unix time, harvest alarms, deep sleep scheduling | -| `solar.c/h` | Simple FLOAT/BULK solar charge state machine | -| `sc_err.h` | Error code definitions | -| `log_test.c/h` | Flash log unit tests | -| `hard_ui.c` | Legacy LCD code (unused/obsolete) | - ---- - -## Control FSM States - -``` -STATE_IDLE -STATE_MOVE_START_DELAY (1s) -STATE_JACK_UP_START (detect current spike → jack engaged) -STATE_JACK_UP (continue until timer/e-fuse) -STATE_DRIVE_START_DELAY (1s) -STATE_DRIVE (encoder-based distance control) -STATE_DRIVE_END_DELAY (1s) -STATE_JACK_DOWN (reverse until e-fuse/sensor) -→ back to STATE_IDLE - -STATE_UNDO_JACK_START (emergency: reverse jack immediately) -STATE_UNDO_JACK (run until e-fuse/sensor) -→ back to STATE_IDLE - -CAL_JACK_DELAY / CAL_JACK_MOVE (jack calibration sequence) -CAL_DRIVE_DELAY / CAL_DRIVE_MOVE (drive calibration sequence) -``` - -**Guards before START:** -- Remaining distance > 0 (leash protection) -- Battery V ≥ `LOW_PROTECTION_V` (default 10V) -- Safety sensor active (debounced stable) -- All e-fuses not tripped - -**FSM Loop (20ms tick in `control_task()`):** -1. `process_bridge_current()` — ADC → EMA → auto-zero → e-fuse -2. `process_battery_voltage()` — ADC → EMA -3. `sensors_check()` — drain ISR queue, update counters/debounce -4. State machine transitions (timer + sensor + efuse checks) -5. `driveRelays()` — write relay output from current state -6. `send_fsm_log()` — 39-byte timestamped entry to flash - ---- - -## E-Fuse Algorithm (`power_mgmt.c`) - -Per bridge, each 20ms tick: -1. Raw ADC → EMA filter (α = `ADC_ALPHA_ISENS`) -2. Auto-zero: learn zero offset when motor is off + grace period expired -3. Grace period: 250ms after relay closes (ignores startup inrush) -4. **Instant trip:** I ≥ `EFUSE_KINST` × I_nom (default 2×) -5. **Thermal trip:** heat accumulates as I²·Δt; dissipates at τ_cool rate -6. **Auto-reset:** after `EFUSE_TCOOL` seconds of cooling (default 5s) - ---- - -## Safety Sensor Debouncing (Asymmetric) - -``` -LOW (safe): 1000ms make time → slow to declare safe (SAFETY_MAKE_US) -HIGH (break): 300ms break time → fast to kill operation (SAFETY_BREAK_US) -``` - -Safety break → immediate `STATE_UNDO_JACK_START`. - ---- - -## Communication Interfaces - -### WiFi (softAP) -- SSID/password/channel configurable via params (`WIFI_SSID`, `WIFI_PASS`, `WIFI_CHANNEL`) -- mDNS hostname: `sc.local` -- Captive portal DNS: all queries → 192.168.4.1 -- HTTP port 80 - -### HTTP API (port 80) -| Endpoint | Method | Description | -|----------|--------|-------------| -| `/` | GET | Embedded gzip HTML webpage | -| `/get` | GET | JSON system status | -| `/set` | POST | JSON commands + parameter updates | -| `/log` | GET | Binary log download (4B JSON len + JSON + 8B offsets + log data) | - -### UART (115200 8N1) -- `GET` → same as HTTP GET /get -- `POST: {json}` → same as HTTP POST /set -- `HELP` → command reference -- Shares `comms_handle_get()` / `comms_handle_post()` with HTTP - -### 433MHz RF (GPIO25, RMT) -- 24-bit OOK codes (P_HIGH≈1040µs, P_LOW≈340µs, margin 70µs) -- 8 stored keycodes → FSM_OVERRIDE_* commands -- Learn mode: capture next RX → temp buffer → user commits via web - -### Bluetooth HID Host -- Scans for BLE HID devices (service UUID 0x1812) -- Tries saved BDA first, then scans for best RSSI -- Button mapping: - - VOL_UP → Jack Up (override pulse) - - VOL_DOWN → Jack Down - - PREV → Drive Reverse - - NEXT → Drive Forward - ---- - -## Storage Layout - -**Flash partition "storage":** -``` -0x0000 – 0x0FFF Parameters (4 sectors, CRC32-protected, 47 params) -0x1000 – end Circular log buffer (head/tail tracked) -``` - -**Log entry format (39 bytes typical):** -``` -[0:8] Timestamp ms (u64 BE) -[8:12] Battery voltage (f32) -[12:16] Drive current (f32) -[16:20] Jack current (f32) -[20:24] Aux current (f32) -[24:26] Drive encoder count (i16) -[26] Sensor states (packed) -[27:31] Drive heat (f32) -[31:35] Jack heat (f32) -[35:39] Aux heat (f32) -``` - -**Key Parameters:** -- Motion: `DRIVE_DIST`, `JACK_DIST`, `DRIVE_KT`, `JACK_KT`, `DRIVE_KE` -- E-fuse: `EFUSE_INOM_1/2/3`, `EFUSE_HEAT_THRESH`, `EFUSE_KINST`, `EFUSE_TCOOL` -- Safety: `SAFETY_BREAK_US`, `SAFETY_MAKE_US`, `LOW_PROTECTION_V` -- RF: `KEYCODE_0` … `KEYCODE_7` -- WiFi: `WIFI_SSID`, `WIFI_PASS`, `WIFI_CHANNEL` -- Schedule: `NUM_MOVES`, `MOVE_START`, `MOVE_END` (seconds-since-midnight) - ---- - -## RTC & 32.768 kHz Crystal - -**Crystal:** Standard 32.768 kHz (32768 Hz = 2¹⁵ Hz) tuning-fork watch crystal on GPIO32/GPIO33. This frequency is universal for RTCs because it divides to exactly 1 Hz with a 15-bit binary counter. - -**sdkconfig.defaults settings:** -- `CONFIG_RTC_CLK_SRC_EXT_CRYS=y` — selects the external crystal as the RTC slow clock source instead of the internal ~150 kHz RC oscillator -- `CONFIG_ESP32_RTC_EXT_CRYST_ADDIT_CURRENT_V2=y` — enables extra drive current during the crystal startup window; required for high-ESR tuning-fork crystals (e.g. CM315D32768DZFT ~70 kΩ ESR) - -**Known startup failure mode:** On power-on, the ESP32 bootloader attempts to calibrate the crystal. If it fails to detect oscillation within its calibration window, it logs `W: 32 kHz XTAL not found, switching to internal 150 kHz oscillator` and falls back to the RC oscillator. The RC oscillator has ±5% accuracy, producing up to ~180 s/hr of RTC drift — this completely breaks harvest scheduling. - -**Firmware mitigation (`rtc_xtal_init()` in `rtc.c`):** If `rtc_clk_slow_src_get()` does not return `SOC_RTC_SLOW_CLK_SRC_XTAL32K` at startup, the code applies a manual bootstrap: `rtc_clk_32k_bootstrap(20000)` (~600 ms of extra drive current at 32 kHz cycles), waits 500 ms for oscillation to stabilise, then calls `rtc_clk_slow_src_set(SOC_RTC_SLOW_CLK_SRC_XTAL32K)` to switch explicitly. Success or failure is logged via `ESP_LOGI/LOGE`. - -**Diagnosing crystal issues:** Run `RTCDEBUG` over UART and check `slow_clk_src`. It reports either `XTAL32K (OK)` or `NOT XTAL32K — check crystal!`. The `logtool/rtc_test.py` script automates this and runs multi-cycle drift tests. - -**Time persistence across deep sleep:** `rtc_backup_s` and `rtc_sleep_entry_s` are `RTC_DATA_ATTR` (survive deep sleep). On wakeup, `rtc_restore_time()` adds exactly `DEEP_SLEEP_US / 1e6` seconds to `rtc_sleep_entry_s` to reconstruct the correct time without an NTP sync. - ---- - -## Power Management - -- **Battery voltage:** GPIO39, divider → `V = raw × 0.00767 + 0.4` -- **Solar charger:** GPIO26 (RTC hold) — FLOAT/BULK FSM, bulk for 20s when V < 5V for 5s -- **Inactivity shutdown:** 180s → deep sleep -- **Deep sleep wakeup:** RTC timer (120s), RTC alarm (next harvest), EXT0 GPIO13 (button) -- **RTC_DATA_ATTR:** FSM state, errors, alarm times, charge state — survive deep sleep - ---- - -## Error Codes (`sc_err.h`) - -```c -SC_ERR_EFUSE_TRIP_1 = 0x201 // Drive overcurrent/overheat -SC_ERR_EFUSE_TRIP_2 = 0x202 // Jack -SC_ERR_EFUSE_TRIP_3 = 0x203 // Aux -SC_ERR_SAFETY_TRIP = 0x210 // Safety sensor break -SC_ERR_LEASH_HIT = 0x211 // Distance limit reached -SC_ERR_RTC_NOT_SET = 0x220 // Clock not synchronized -SC_ERR_LOW_BATTERY = 0x230 // Voltage below threshold -``` - ---- - -## Build System - -- **Framework:** ESP-IDF (>=4.1.0) -- **Component deps** (`idf_component.yml`): `espressif/mdns`, `joltwallet/littlefs`, `esp-idf-lib/tca95x5` -- **IDF requires:** `driver`, `esp_http_server`, `esp_netif`, `lwip`, `json`, `esp_timer`, `esp_adc`, `app_update`, `esp_wifi`, `nvs_flash`, `mdns`, `bt`, `esp_hid` -- **Webpage:** `webpage.html` → `webpage_compile.py` → `webpage_gzip.h` (embedded gzip binary). **Must re-run `webpage_compile.py` after any HTML edit before building.** -- **Version:** `version.h.in` filled by CMake from git tags → `FIRMWARE_VERSION`, `BUILD_DATE` -- **Factory reset:** Hold GPIO13 button on cold boot → full parameter + log erase +- **Naming:** `snake_case` functions with module prefix (`fsm_init`, `i2c_poll_buttons`); `UPPER_SNAKE_CASE` constants/enums +- **Module pattern:** `.c` / `.h` pairs; headers expose only public API +- **Concurrency:** FSM commands via `xQueueSend`; log writes via async queue; GPIO ISR → minimal work → sensor queue +- **State machine pattern:** transitions in one `switch`, relay outputs in a second `switch` (separated) +- **Watchdog:** `esp_task_wdt_add/reset` in each task, 10s timeout +- **Logging:** `ESP_LOGI(TAG, ...)` per module; flash circular log for telemetry +- **No dynamic allocation** in ISR or high-priority paths --- @@ -323,15 +101,3 @@ All fields optional. `parameters` is a flat object of param key → value. 1. Add `` in HTML 2. Add key to `WIFI_PARAM_KEYS` (or equivalent filter set) in `updateParamTable()` so it isn't duplicated in the raw table 3. Optionally add a dedicated apply function following `applyWifiSettings()` pattern - ---- - -## Conventions - -- **Naming:** `snake_case` functions with module prefix (`fsm_init`, `i2c_poll_buttons`); `UPPER_SNAKE_CASE` constants/enums -- **Module pattern:** `.c` / `.h` pairs; headers expose only public API -- **Concurrency:** FSM commands via `xQueueSend`; log writes via async queue; GPIO ISR → minimal work → sensor queue -- **State machine pattern:** transitions in one `switch`, relay outputs in a second `switch` (separated) -- **Watchdog:** `esp_task_wdt_add/reset` in each task, 10s timeout -- **Logging:** `ESP_LOGI(TAG, ...)` per module; flash circular log for telemetry -- **No dynamic allocation** in ISR or high-priority paths diff --git a/README.md b/README.md index 08e615e..73f9a66 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,278 @@ -SC-F001 -======= +# SC-F001 Firmware -Firmware for SC-B001 \ No newline at end of file +**Solar-powered automated crop harvesting robot** built on the ESP32. Drives a carriage horizontally via a drive motor, lifts/lowers a cutting head via a jack motor, with an auxiliary "fluffer" motor always running during operation. The firmware handles motor sequencing, safety interlocks, remote control, data logging, and a WiFi web interface. + +**Primary operational cycle:** Idle → Move Start Delay → Jack Up → Drive → Jack Down → Idle + +--- + +## Hardware Platform + +**MCU:** ESP32 (Xtensa dual-core), ESP-IDF framework + +**GPIO Map:** +| GPIO | Function | +|------|----------| +| 13 | Button interrupt (active low, pull-up) — also EXT0 wakeup | +| 14 | Jack position sensor / encoder | +| 16 | Drive encoder | +| 19 | Aux sensor 2 (reserved) | +| 21/22 | I2C SDA/SCL (400kHz) → TCA9555 I/O expander | +| 25 | 433MHz RF receiver (RMT input) | +| 26 | Solar charger bulk enable (RTC GPIO, holds across deep sleep) | +| 27 | Safety sensor (active low) | +| 32/33 | External 32.768 kHz RTC crystal (standard watch crystal, 2¹⁵ Hz) | +| 36 (VP) | ADC: drive current sense | +| 39 (VN) | ADC: battery voltage | +| 34 | ADC: jack current sense | +| 35 | ADC: aux current sense | + +**TCA9555 (I2C at 0x20):** +- Port 0 (input): 2 physical buttons + 2 additional inputs +- Port 1 (output): 3× H-bridge relay pairs (DRIVE, JACK, AUX) + LEDs + +**Motor / Bridge Specs:** +- `BRIDGE_DRIVE` — 100A max, ACS37220 sense chip (13.2 mV/A, inverted polarity) +- `BRIDGE_JACK` — 30A max, ACS37042 sense chip (44 mV/A) +- `BRIDGE_AUX` — 30A max, ACS37042 sense chip (44 mV/A) + +--- + +## Software Architecture + +``` +app_main() +├── rtc_xtal_init() RTC crystal + EXT0 wakeup + sleep wakeup check +├── i2c_init() TCA9555 init (relays off, LEDs off) +├── adc_init() ADC1 calibration (12dB attenuation, line-fit) +├── storage_init() Flash params + circular log buffer +├── solar_run_fsm() (called in main loop too) +├── uart_init() Serial JSON API task +├── rf_433_init() 433MHz RMT receiver task +├── bt_hid_init() BLE HID host scanner task +├── fsm_init() Control FSM task (priority 10, 20ms tick) +└── webserver_init() WiFi softAP + HTTP + mDNS + DNS + +Main loop (50ms): + i2c_poll_buttons() + fsm_request() based on button events + solar_run_fsm() + driveLEDs() status animation + rtc_check_shutdown_timer() → deep sleep on inactivity (180s) +``` + +**FreeRTOS Tasks:** +| Task | Created by | Priority | Tick | Purpose | +|------|-----------|----------|------|---------| +| `app_main` (main loop) | system | 1 (default) | 50ms | Button polling, LED animation, solar FSM, shutdown timer | +| `control_task` | `fsm_init()` | 10 | 20ms | FSM state machine, relay control, ADC current monitoring, e-fuse | +| UART task | `uart_init()` | default | event-driven | Serial JSON command processing | +| RF 433 task | `rf_433_init()` | default | event-driven | RMT receive + keycode matching | +| BT HID task | `bt_hid_init()` | default | event-driven | BLE HID host scanning + button mapping | +| httpd workers | `webserver_init()` | default | event-driven | HTTP request handling (multiple workers spawned by esp_http_server) | + +--- + +## Key Files + +| File | Purpose | +|------|---------| +| `main.c` | Entry point, 50ms main loop, factory reset, LED animation | +| `control_fsm.c/h` | State machine, relay control, current monitoring, calibration | +| `power_mgmt.c/h` | ADC reading, e-fuse thermal algorithm, battery voltage | +| `sensors.c/h` | GPIO ISR-based sensor debouncing, encoder counters | +| `i2c.c/h` | TCA9555 relay/LED/button control | +| `storage.c/h` | 47-param NVM table + circular binary log buffer | +| `comms.c/h` | Unified GET/POST JSON API (shared by HTTP and UART) | +| `webserver.c/h` | WiFi softAP, HTTP server, embedded gzip webpage | +| `uart_comms.c/h` | Serial JSON interface (115200 8N1) | +| `rf_433.c/h` | 433MHz OOK receiver, keycode learn/match | +| `bt_hid.c/h` | BLE HID host, media remote button mapping | +| `rtc.c/h` | Unix time, harvest alarms, deep sleep scheduling | +| `solar.c/h` | Simple FLOAT/BULK solar charge state machine | +| `sc_err.h` | Error code definitions | +| `log_test.c/h` | Flash log unit tests | +| `hard_ui.c` | Legacy LCD code (unused/obsolete) | + +--- + +## Control FSM States + +``` +STATE_IDLE +STATE_MOVE_START_DELAY (1s) +STATE_JACK_UP_START (detect current spike → jack engaged) +STATE_JACK_UP (continue until timer/e-fuse) +STATE_DRIVE_START_DELAY (1s) +STATE_DRIVE (encoder-based distance control) +STATE_DRIVE_END_DELAY (1s) +STATE_JACK_DOWN (reverse until e-fuse/sensor) +→ back to STATE_IDLE + +STATE_UNDO_JACK_START (emergency: reverse jack immediately) +STATE_UNDO_JACK (run until e-fuse/sensor) +→ back to STATE_IDLE + +CAL_JACK_DELAY / CAL_JACK_MOVE (jack calibration sequence) +CAL_DRIVE_DELAY / CAL_DRIVE_MOVE (drive calibration sequence) +``` + +**Guards before START:** +- Remaining distance > 0 (leash protection) +- Battery V ≥ `LOW_PROTECTION_V` (default 10V) +- Safety sensor active (debounced stable) +- All e-fuses not tripped + +**FSM Loop (20ms tick in `control_task()`):** +1. `process_bridge_current()` — ADC → EMA → auto-zero → e-fuse +2. `process_battery_voltage()` — ADC → EMA +3. `sensors_check()` — drain ISR queue, update counters/debounce +4. State machine transitions (timer + sensor + efuse checks) +5. `driveRelays()` — write relay output from current state +6. `send_fsm_log()` — 39-byte timestamped entry to flash + +--- + +## E-Fuse Algorithm (`power_mgmt.c`) + +Per bridge, each 20ms tick: +1. Raw ADC → EMA filter (α = `ADC_ALPHA_ISENS`) +2. Auto-zero: learn zero offset when motor is off + grace period expired +3. Grace period: 250ms after relay closes (ignores startup inrush) +4. **Instant trip:** I ≥ `EFUSE_KINST` × I_nom (default 2×) +5. **Thermal trip:** heat accumulates as I²·Δt; dissipates at τ_cool rate +6. **Auto-reset:** after `EFUSE_TCOOL` seconds of cooling (default 5s) + +--- + +## Safety Sensor Debouncing (Asymmetric) + +``` +LOW (safe): 1000ms make time → slow to declare safe (SAFETY_MAKE_US) +HIGH (break): 300ms break time → fast to kill operation (SAFETY_BREAK_US) +``` + +Safety break → immediate `STATE_UNDO_JACK_START`. + +--- + +## Communication Interfaces + +### WiFi (softAP) +- SSID/password/channel configurable via params (`WIFI_SSID`, `WIFI_PASS`, `WIFI_CHANNEL`) +- mDNS hostname: `sc.local` +- Captive portal DNS: all queries → 192.168.4.1 +- HTTP port 80 + +### HTTP API (port 80) +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/` | GET | Embedded gzip HTML webpage | +| `/get` | GET | JSON system status | +| `/set` | POST | JSON commands + parameter updates | +| `/log` | GET | Binary log download (4B JSON len + JSON + 8B offsets + log data) | + +### UART (115200 8N1) +- `GET` → same as HTTP GET /get +- `POST: {json}` → same as HTTP POST /set +- `HELP` → command reference +- Shares `comms_handle_get()` / `comms_handle_post()` with HTTP + +### 433MHz RF (GPIO25, RMT) +- 24-bit OOK codes (P_HIGH≈1040µs, P_LOW≈340µs, margin 70µs) +- 8 stored keycodes → FSM_OVERRIDE_* commands +- Learn mode: capture next RX → temp buffer → user commits via web + +### Bluetooth HID Host +- Scans for BLE HID devices (service UUID 0x1812) +- Tries saved BDA first, then scans for best RSSI +- Button mapping: + - VOL_UP → Jack Up (override pulse) + - VOL_DOWN → Jack Down + - PREV → Drive Reverse + - NEXT → Drive Forward + +--- + +## Storage Layout + +**Flash partition "storage":** +``` +0x0000 – 0x0FFF Parameters (4 sectors, CRC32-protected, 47 params) +0x1000 – end Circular log buffer (head/tail tracked) +``` + +**Log entry format (39 bytes typical):** +``` +[0:8] Timestamp ms (u64 BE) +[8:12] Battery voltage (f32) +[12:16] Drive current (f32) +[16:20] Jack current (f32) +[20:24] Aux current (f32) +[24:26] Drive encoder count (i16) +[26] Sensor states (packed) +[27:31] Drive heat (f32) +[31:35] Jack heat (f32) +[35:39] Aux heat (f32) +``` + +**Key Parameters:** +- Motion: `DRIVE_DIST`, `JACK_DIST`, `DRIVE_KT`, `JACK_KT`, `DRIVE_KE` +- E-fuse: `EFUSE_INOM_1/2/3`, `EFUSE_HEAT_THRESH`, `EFUSE_KINST`, `EFUSE_TCOOL` +- Safety: `SAFETY_BREAK_US`, `SAFETY_MAKE_US`, `LOW_PROTECTION_V` +- RF: `KEYCODE_0` … `KEYCODE_7` +- WiFi: `WIFI_SSID`, `WIFI_PASS`, `WIFI_CHANNEL` +- Schedule: `NUM_MOVES`, `MOVE_START`, `MOVE_END` (seconds-since-midnight) + +--- + +## RTC & 32.768 kHz Crystal + +**Crystal:** Standard 32.768 kHz (32768 Hz = 2¹⁵ Hz) tuning-fork watch crystal on GPIO32/GPIO33. This frequency is universal for RTCs because it divides to exactly 1 Hz with a 15-bit binary counter. + +**sdkconfig.defaults settings:** +- `CONFIG_RTC_CLK_SRC_EXT_CRYS=y` — selects the external crystal as the RTC slow clock source instead of the internal ~150 kHz RC oscillator +- `CONFIG_ESP32_RTC_EXT_CRYST_ADDIT_CURRENT_V2=y` — enables extra drive current during the crystal startup window; required for high-ESR tuning-fork crystals (e.g. CM315D32768DZFT ~70 kΩ ESR) + +**Known startup failure mode:** On power-on, the ESP32 bootloader attempts to calibrate the crystal. If it fails to detect oscillation within its calibration window, it logs `W: 32 kHz XTAL not found, switching to internal 150 kHz oscillator` and falls back to the RC oscillator. The RC oscillator has ±5% accuracy, producing up to ~180 s/hr of RTC drift — this completely breaks harvest scheduling. + +**Firmware mitigation (`rtc_xtal_init()` in `rtc.c`):** If `rtc_clk_slow_src_get()` does not return `SOC_RTC_SLOW_CLK_SRC_XTAL32K` at startup, the code applies a manual bootstrap: `rtc_clk_32k_bootstrap(20000)` (~600 ms of extra drive current at 32 kHz cycles), waits 500 ms for oscillation to stabilise, then calls `rtc_clk_slow_src_set(SOC_RTC_SLOW_CLK_SRC_XTAL32K)` to switch explicitly. Success or failure is logged via `ESP_LOGI/LOGE`. + +**Diagnosing crystal issues:** Run `RTCDEBUG` over UART and check `slow_clk_src`. It reports either `XTAL32K (OK)` or `NOT XTAL32K — check crystal!`. The `logtool/rtc_test.py` script automates this and runs multi-cycle drift tests. + +**Time persistence across deep sleep:** `rtc_backup_s` and `rtc_sleep_entry_s` are `RTC_DATA_ATTR` (survive deep sleep). On wakeup, `rtc_restore_time()` adds exactly `DEEP_SLEEP_US / 1e6` seconds to `rtc_sleep_entry_s` to reconstruct the correct time without an NTP sync. + +--- + +## Power Management + +- **Battery voltage:** GPIO39, divider → `V = raw × 0.00767 + 0.4` +- **Solar charger:** GPIO26 (RTC hold) — FLOAT/BULK FSM, bulk for 20s when V < 5V for 5s +- **Inactivity shutdown:** 180s → deep sleep +- **Deep sleep wakeup:** RTC timer (120s), RTC alarm (next harvest), EXT0 GPIO13 (button) +- **RTC_DATA_ATTR:** FSM state, errors, alarm times, charge state — survive deep sleep + +--- + +## Error Codes (`sc_err.h`) + +```c +SC_ERR_EFUSE_TRIP_1 = 0x201 // Drive overcurrent/overheat +SC_ERR_EFUSE_TRIP_2 = 0x202 // Jack +SC_ERR_EFUSE_TRIP_3 = 0x203 // Aux +SC_ERR_SAFETY_TRIP = 0x210 // Safety sensor break +SC_ERR_LEASH_HIT = 0x211 // Distance limit reached +SC_ERR_RTC_NOT_SET = 0x220 // Clock not synchronized +SC_ERR_LOW_BATTERY = 0x230 // Voltage below threshold +``` + +--- + +## Build System + +- **Framework:** ESP-IDF (>=4.1.0) +- **Component deps** (`idf_component.yml`): `espressif/mdns`, `joltwallet/littlefs`, `esp-idf-lib/tca95x5` +- **IDF requires:** `driver`, `esp_http_server`, `esp_netif`, `lwip`, `json`, `esp_timer`, `esp_adc`, `app_update`, `esp_wifi`, `nvs_flash`, `mdns`, `bt`, `esp_hid` +- **Webpage:** `webpage.html` → `webpage_compile.py` → `webpage_gzip.h` (embedded gzip binary). **Must re-run `webpage_compile.py` after any HTML edit before building.** +- **Version:** `version.h.in` filled by CMake from git tags → `FIRMWARE_VERSION`, `BUILD_DATE` +- **Factory reset:** Hold GPIO13 button on cold boot → full parameter + log erase diff --git a/TODO.md b/TODO.md index 1f58510..de6e573 100644 --- a/TODO.md +++ b/TODO.md @@ -1,55 +1,55 @@ # SC-F001 Firmware — TODO -- [ ] sdkconfig audit - - [ ] Enable `CONFIG_ESP_TASK_WDT_PANIC=y` (required for OTA rollback reset counter to work on WDT hangs) - - [ ] Verify `CONFIG_FREERTOS_CHECK_STACKOVERFLOW=2` is set (currently canary — confirmed) - - [ ] Verify `CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT` is set (currently set — confirmed) - - [ ] Confirm brownout detector level (~2.43V) is appropriate for 12V battery system with regulator - - [ ] Research sdkconfig management best practices; document in CLAUDE.md -- [ ] Fix managed_components: remove unused deps, pin versions in `idf_component.yml`; document in CLAUDE.md -- [ ] OTA rollback via consecutive-reset counter - - [ ] Add `RTC_DATA_ATTR uint8_t reset_counter` — increment on boot, clear after successful health check - - [ ] On counter ≥ 5, call `esp_ota_mark_app_invalid_rollback_and_reboot()` - - [ ] After POST passes and FSM starts, call `esp_ota_mark_app_valid_cancel_rollback()` - - [ ] Decide what "health check passes" means (POST passes? 30s uptime? first successful FSM cycle?) -- [ ] Critical init failures (ADC, storage, log, I2C, FSM, sensors) should `esp_restart()` — this feeds the OTA rollback reset counter -- [ ] Non-critical init failures (wifi, webserver, RF, BT) should log a `LOG_TYPE_ERROR` entry and attempt retry - - [ ] WiFi/BT already have restart paths (`webserver_restart_wifi()`, `bt_hid_resume()`) — wire these into a retry-on-failure path at boot, not just soft idle exit -- [ ] Power-on self-test (POST) — run after all inits, before FSM starts; log results; feed OTA health check - - [ ] ADC: read all 4 channels twice with short delay, flag if frozen or out of range (battery 5–25V, currents 0–150A) - - [ ] I2C: verify TCA9555 responds (read port 0) - - [ ] Flash: write-read-verify test on last sector of storage partition -- [ ] Parameter validation - - [ ] Add per-param bounds to `PARAM_LIST` macro (min, max, flags) - - [ ] NaN/Inf → reset to default; out-of-range → clamp to min/max - - [ ] Enforce validation inside `commit_params()` (covers both `storage_init()` load and `/set` POST) - - [ ] Audit for anywhere params are set without an immediate `commit_params()` call - - [ ] Audit abandoned parameters (e.g. jack current) — add comments marking them deprecated -- [ ] Factory reset: erase entire storage partition (not just params), require 10s button hold, LED indication (flash all → hold solid once triggered) -- [ ] Ensure RTC_DATA_ATTR variables survive panics/WDT resets - - [ ] Verify `sync_unix_us`, `sync_rtc_us`, `rtc_set` (time) are not corrupted by any init path - - [ ] Verify `remaining_distance`, `fsm_error` (FSM state) are not zeroed except by intentional reset - - [ ] Verify `log_head_offset`, `log_tail_offset` stay consistent after crash (no partial writes) -- [ ] Measure flash log write duration (bracket with `esp_timer_get_time()`, compare to WDT timeout of 5s) -- [ ] WiFi STA mode with event-group signaling - - [ ] Try connecting to saved STA network first, fall back to softAP on failure/timeout - - [ ] Add `EventGroupHandle_t` with `WIFI_READY_BIT` (set when STA connected or softAP up) and `BT_READY_BIT` (set when BT scan task starts) - - [ ] Replace blind 500ms `vTaskDelay` on alarm wake with `xEventGroupWaitBits()` + timeout - - [ ] Use same event group in `soft_idle_exit()` path -- [ ] Verify `sensors_init()` placement and ISR safety - - [ ] Confirm `sensors_init()` is safe to call from `app_main()` (research says yes — creates queue + installs ISR service, no task-context dependency) - - [ ] Decide: move to main.c (simpler) or keep in `control_task()` (current) — either way, remove the dead commented-out call in main.c and add a clarifying comment - - [ ] Audit all ISRs are IRAM-safe: no `ESP_LOGx`, `printf`, `malloc`, or flash access — only `xQueueSendFromISR()` - - [ ] Handle `sensors_init()` failure as critical (→ reboot) -- [ ] Confirm whether external RTC crystal can be dropped (device never enters deep sleep now) — if yes, remove `rtc_xtal_init()` and related sdkconfig entries; if no, document why it must stay -- [ ] Remove `rtc_wakeup_cause()` call (informational only, no longer needed) -- [ ] Confirm `rtc_check_shutdown_timer()` uses signed subtraction — then remove the esp_timer overflow TODO comment (int64_t overflows after 292K years) -- [ ] Extract pure logic (e-fuse thermal model, param serialization, sensor debounce) into host-testable modules with Unity/CMock -- [ ] UART integration test framework: Python runner + ESP-side test commands -- [test] Logtool GUI output (matplotlib) -- [test] Verify naming convention adherence across codebase -- [test] Verify WiFi SSID rename triggers comms reboot -- [ ] Documentation restructure - - [ ] Move project/hardware documentation from CLAUDE.md → README.md; keep CLAUDE.md for AI-specific instructions and conventions only - - [ ] Document all FreeRTOS tasks and priorities in README.md - - [ ] Add terse comments to FSM state transitions in `control_fsm.c` (focus on "why", not "what") +1. - [clauded] sdkconfig audit + - [clauded] Enable `CONFIG_ESP_TASK_WDT_PANIC=y` — added to sdkconfig.defaults and sdkconfig + - [clauded] Verify `CONFIG_FREERTOS_CHECK_STACKOVERFLOW=2` — confirmed canary method active + - [clauded] Verify `CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT` — confirmed active + - [clauded] Confirm brownout detector level — ~2.43V is correct (ESP32 rail protection; battery low-V handled by FSM's `LOW_PROTECTION_V`) + - [clauded] Research sdkconfig management best practices — documented in CLAUDE.md "sdkconfig Management" section +2. - [ ] Fix managed_components: remove unused deps, pin versions in `idf_component.yml`; document in CLAUDE.md +3. - [ ] OTA rollback via consecutive-reset counter + - [ ] Add `RTC_DATA_ATTR uint8_t reset_counter` — increment on boot, clear after successful health check + - [ ] On counter ≥ 5, call `esp_ota_mark_app_invalid_rollback_and_reboot()` + - [ ] After POST passes and FSM starts, call `esp_ota_mark_app_valid_cancel_rollback()` + - [ ] Decide what "health check passes" means (POST passes? 30s uptime? first successful FSM cycle?) +4. - [ ] Critical init failures (ADC, storage, log, I2C, FSM, sensors) should `esp_restart()` — this feeds the OTA rollback reset counter +5. - [ ] Non-critical init failures (wifi, webserver, RF, BT) should log a `LOG_TYPE_ERROR` entry and attempt retry + - [ ] WiFi/BT already have restart paths (`webserver_restart_wifi()`, `bt_hid_resume()`) — wire these into a retry-on-failure path at boot, not just soft idle exit +6. - [ ] Power-on self-test (POST) — run after all inits, before FSM starts; log results; feed OTA health check + - [ ] ADC: read all 4 channels twice with short delay, flag if frozen or out of range (battery 5–25V, currents 0–150A) + - [ ] I2C: verify TCA9555 responds (read port 0) + - [ ] Flash: write-read-verify test on last sector of storage partition +7. - [ ] Parameter validation + - [ ] Add per-param bounds to `PARAM_LIST` macro (min, max, flags) + - [ ] NaN/Inf → reset to default; out-of-range → clamp to min/max + - [ ] Enforce validation inside `commit_params()` (covers both `storage_init()` load and `/set` POST) + - [ ] Audit for anywhere params are set without an immediate `commit_params()` call + - [ ] Audit abandoned parameters (e.g. jack current) — add comments marking them deprecated +8. - [ ] Factory reset: erase entire storage partition (not just params), require 10s button hold, LED indication (flash all → hold solid once triggered) +9. - [ ] Ensure RTC_DATA_ATTR variables survive panics/WDT resets + - [ ] Verify `sync_unix_us`, `sync_rtc_us`, `rtc_set` (time) are not corrupted by any init path + - [ ] Verify `remaining_distance`, `fsm_error` (FSM state) are not zeroed except by intentional reset + - [ ] Verify `log_head_offset`, `log_tail_offset` stay consistent after crash (no partial writes) +10. - [ ] Measure flash log write duration (bracket with `esp_timer_get_time()`, compare to WDT timeout of 5s) +11. - [ ] WiFi STA mode with event-group signaling + - [ ] Try connecting to saved STA network first, fall back to softAP on failure/timeout + - [ ] Add `EventGroupHandle_t` with `WIFI_READY_BIT` (set when STA connected or softAP up) and `BT_READY_BIT` (set when BT scan task starts) + - [ ] Replace blind 500ms `vTaskDelay` on alarm wake with `xEventGroupWaitBits()` + timeout + - [ ] Use same event group in `soft_idle_exit()` path +12. - [ ] Verify `sensors_init()` placement and ISR safety + - [ ] Confirm `sensors_init()` is safe to call from `app_main()` (research says yes — creates queue + installs ISR service, no task-context dependency) + - [ ] Decide: move to main.c (simpler) or keep in `control_task()` (current) — either way, remove the dead commented-out call in main.c and add a clarifying comment + - [ ] Audit all ISRs are IRAM-safe: no `ESP_LOGx`, `printf`, `malloc`, or flash access — only `xQueueSendFromISR()` + - [ ] Handle `sensors_init()` failure as critical (→ reboot) +13. - [ ] Confirm whether external RTC crystal can be dropped (device never enters deep sleep now) — if yes, remove `rtc_xtal_init()` and related sdkconfig entries; if no, document why it must stay +14. - [ ] Remove `rtc_wakeup_cause()` call (informational only, no longer needed) +15. - [ ] Confirm `rtc_check_shutdown_timer()` uses signed subtraction — then remove the esp_timer overflow TODO comment (int64_t overflows after 292K years) +16. - [ ] Extract pure logic (e-fuse thermal model, param serialization, sensor debounce) into host-testable modules with Unity/CMock +17. - [ ] UART integration test framework: Python runner + ESP-side test commands +18. - [test] Logtool GUI output (matplotlib) +19. - [test] Verify naming convention adherence across codebase +20. - [test] Verify WiFi SSID rename triggers comms reboot +21. - [clauded] Documentation restructure + - [clauded] Move project/hardware documentation from CLAUDE.md → README.md; keep CLAUDE.md for AI-specific instructions and conventions only + - [clauded] Document all FreeRTOS tasks and priorities in README.md + - [clauded] Add terse comments to FSM state transitions in `control_fsm.c` (focus on "why", not "what") diff --git a/main/control_fsm.c b/main/control_fsm.c index 6d215f4..1ecbdcf 100644 --- a/main/control_fsm.c +++ b/main/control_fsm.c @@ -6,7 +6,7 @@ */ -// TODO: Comment, and even better, produce a README for this. +// See README.md for FSM documentation (states, guards, timing). #include "control_fsm.h" #include "esp_task_wdt.h" @@ -340,28 +340,33 @@ void control_task(void *param) { if (!enabled) break; /**** STATE TRANSITIONS ****/ + // Every active state checks safety first — break triggers UNDO_JACK (emergency lower). + // Normal cycle: IDLE → DELAY → JACK_UP_START → JACK_UP → DRIVE → JACK_DOWN → IDLE switch (current_state) { case STATE_IDLE: break; + case STATE_MOVE_START_DELAY: + // 1s pause before raising jack — lets operator abort after pressing start if (!get_is_safe()) { fsm_error = SC_ERR_SAFETY_TRIP; - current_state = STATE_IDLE; + current_state = STATE_IDLE; // haven't raised jack yet, safe to just stop log = true; } else if (timer_done()) { current_state = STATE_JACK_UP_START; - set_timer(JACK_TIME / 2); // First phase is half of total jack time + set_timer(JACK_TIME / 2); // first phase: detect engagement (half of total jack time) jack_start_us = fsm_now; } break; + case STATE_JACK_UP_START: + // Detect when jack engages the load (current spike, efuse, or timeout) if (!get_is_safe()) { fsm_error = SC_ERR_SAFETY_TRIP; current_state = STATE_UNDO_JACK_START; jack_finish_us = fsm_now; log = true; } else { - if (efuse_get(BRIDGE_JACK)) { ESP_LOGI(TAG, "START->UP BY EFUSE"); current_state = STATE_JACK_UP; @@ -369,7 +374,7 @@ void control_task(void *param) { log = true; set_timer(JACK_TIME); } - + if (get_bridge_overcurrent(BRIDGE_JACK, get_param_value_t(PARAM_JACK_I_UP).f32)) { ESP_LOGI(TAG, "START->UP BY CURRENT"); current_state = STATE_JACK_UP; @@ -377,7 +382,7 @@ void control_task(void *param) { log = true; set_timer(JACK_TIME); } - + if (timer_done()) { ESP_LOGI(TAG, "START->UP BY TIME"); current_state = STATE_JACK_UP; @@ -387,24 +392,27 @@ void control_task(void *param) { } } break; + case STATE_JACK_UP: + // Continue raising until timer or efuse — records finish time for symmetric jack-down if (!get_is_safe()) { fsm_error = SC_ERR_SAFETY_TRIP; current_state = STATE_UNDO_JACK_START; jack_finish_us = fsm_now; set_timer(JACK_DOWN_TIME); log = true; - } else { + } else { if (timer_done() || efuse_get(BRIDGE_JACK)) { - // Track total time including first phase current_state = STATE_DRIVE_START_DELAY; - jack_finish_us = fsm_now; + jack_finish_us = fsm_now; // used to calculate symmetric jack-down duration log = true; set_timer(TRANSITION_DELAY_US); } } break; + case STATE_DRIVE_START_DELAY: + // 1s pause between jack-up and drive — mechanical settling if (!get_is_safe()) { fsm_error = SC_ERR_SAFETY_TRIP; current_state = STATE_UNDO_JACK_START; @@ -414,13 +422,14 @@ void control_task(void *param) { current_state = STATE_DRIVE; log = true; set_timer(DRIVE_TIME); - // Set the encoder counter to track remaining distance in this move + // Encoder counts down from -target to 0 (negative = distance remaining) set_sensor_counter(SENSOR_DRIVE, -DRIVE_DIST); - // Record starting encoder position AFTER setting it move_start_encoder = get_sensor_counter(SENSOR_DRIVE); } break; + case STATE_DRIVE: + // Horizontal travel — stops on timer, encoder target, or efuse trip if (!get_is_safe()) { fsm_error = SC_ERR_SAFETY_TRIP; current_state = STATE_UNDO_JACK_START; @@ -431,25 +440,21 @@ void control_task(void *param) { int32_t ticks_traveled = current_encoder - move_start_encoder; float ke = get_param_value_t(PARAM_DRIVE_KE).f32; float distance_traveled = ticks_traveled / ke; - - // Stop if timer expires OR encoder target reached OR we've used up remaining distance + if (timer_done() || current_encoder > 0) { - // Update remaining distance based on actual travel - //if (current_encoder < 0) - remaining_distance -= this_move_dist; - //else - // remaining_distance -= distance_traveled; - + // Normal completion — deduct planned distance from leash + remaining_distance -= this_move_dist; + current_state = STATE_DRIVE_END_DELAY; log = true; set_timer(TRANSITION_DELAY_US); } - + if (efuse_get(BRIDGE_DRIVE)) { - // Update remaining distance even on fault + // Fault — deduct actual distance traveled (may be partial) remaining_distance -= distance_traveled; if (remaining_distance < 0.0f) remaining_distance = 0.0f; - + fsm_error = SC_ERR_EFUSE_TRIP_1; current_state = STATE_UNDO_JACK_START; set_timer(JACK_DOWN_TIME); @@ -457,7 +462,9 @@ void control_task(void *param) { } } break; + case STATE_DRIVE_END_DELAY: + // 1s pause after drive — then lower jack if (!get_is_safe()) { fsm_error = SC_ERR_SAFETY_TRIP; current_state = STATE_UNDO_JACK_START; @@ -467,79 +474,51 @@ void control_task(void *param) { log = true; } break; + case STATE_JACK_DOWN: - + // Lower jack — stops on efuse (hit ground), position sensor, or timeout if (efuse_get(BRIDGE_JACK)) { - ESP_LOGI(TAG, "DOWN->IDLE BY EFUSE"); - // Current spike detected current_state = STATE_IDLE; log = true; break; - } - - /*if (get_bridge_overcurrent(BRIDGE_JACK, get_param_value_t(PARAM_JACK_I_DOWN).f32)) { - - ESP_LOGI(TAG, "DOWN->IDLE BY OVERCURRENT"); - // Current spike detected - current_state = STATE_IDLE; - log = true; - break; - - } - - if (get_bridge_spike(BRIDGE_JACK, get_param_value_t(PARAM_JACK_IS_DOWN).f32)) { - - ESP_LOGI(TAG, "DOWN->IDLE BY SPIKE"); - // Current spike detected - current_state = STATE_IDLE; - log = true; - break; - - }*/ - + if (get_sensor(SENSOR_JACK)) { ESP_LOGI(TAG, "DOWN->IDLE BY SENSOR"); current_state = STATE_IDLE; log = true; break; } - - if (timer_done() ) { + + if (timer_done()) { ESP_LOGI(TAG, "DOWN->IDLE BY TIME"); current_state = STATE_IDLE; log = true; break; } - break; - - + case STATE_UNDO_JACK_START: - // wait for e-fuse to un-trip + // Emergency: wait for jack efuse to cool, then lower if (!efuse_get(BRIDGE_JACK)) { set_timer(JACK_DOWN_TIME); current_state = STATE_JACK_DOWN; log = true; } break; - - + case STATE_CALIBRATE_JACK_DELAY: - // no way out of this except a command - break; + break; // waiting for user command to begin measurement case STATE_CALIBRATE_JACK_MOVE: if (timer_done()) { current_state = STATE_IDLE; fsm_cal_t = fsm_now - timer_start; } break; - - + case STATE_CALIBRATE_DRIVE_DELAY: - // no way out of this except a command - break; + break; // waiting for user command to begin measurement case STATE_CALIBRATE_DRIVE_MOVE: if (!get_is_safe() || timer_done()) { current_state = STATE_IDLE; @@ -547,7 +526,7 @@ void control_task(void *param) { fsm_cal_e = get_sensor_counter(SENSOR_DRIVE); } break; - + default: break; } diff --git a/sdkconfig b/sdkconfig index 002002a..edec4b0 100644 --- a/sdkconfig +++ b/sdkconfig @@ -1232,7 +1232,7 @@ CONFIG_ESP_INT_WDT_TIMEOUT_MS=300 CONFIG_ESP_INT_WDT_CHECK_CPU1=y CONFIG_ESP_TASK_WDT_EN=y CONFIG_ESP_TASK_WDT_INIT=y -# CONFIG_ESP_TASK_WDT_PANIC is not set +CONFIG_ESP_TASK_WDT_PANIC=y CONFIG_ESP_TASK_WDT_TIMEOUT_S=5 CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=y CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=y diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 20da550..62cb9a1 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -20,3 +20,9 @@ CONFIG_ESP32_RTC_XTAL_BOOTSTRAP_CYCLES=500 # Allow more calibration retries before falling back to RC oscillator. CONFIG_RTC_XTAL_CAL_RETRY=3 CONFIG_ESP32_RTC_XTAL_CAL_RETRY=3 + +# --- Safety & Panic --- + +# WDT timeout triggers a panic (→ reboot + core dump) instead of just logging. +# Required for OTA rollback: a hung task causes a reboot, incrementing the reset counter. +CONFIG_ESP_TASK_WDT_PANIC=y