docs & sdkconfig
This commit is contained in:
312
CLAUDE.md
312
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 `<input id="PARAM_<KEY>" onchange="markChanged(this)"/>` 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
|
||||
|
||||
280
README.md
280
README.md
@@ -1,4 +1,278 @@
|
||||
SC-F001
|
||||
=======
|
||||
# SC-F001 Firmware
|
||||
|
||||
Firmware for SC-B001
|
||||
**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
|
||||
|
||||
58
TODO.md
58
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
|
||||
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?)
|
||||
- [ ] 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
|
||||
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
|
||||
- [ ] Power-on self-test (POST) — run after all inits, before FSM starts; log results; feed OTA health check
|
||||
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
|
||||
- [ ] Parameter validation
|
||||
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
|
||||
- [ ] 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
|
||||
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)
|
||||
- [ ] Measure flash log write duration (bracket with `esp_timer_get_time()`, compare to WDT timeout of 5s)
|
||||
- [ ] WiFi STA mode with event-group signaling
|
||||
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
|
||||
- [ ] Verify `sensors_init()` placement and ISR safety
|
||||
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)
|
||||
- [ ] 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")
|
||||
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")
|
||||
|
||||
@@ -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;
|
||||
@@ -387,7 +392,9 @@ 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;
|
||||
@@ -396,15 +403,16 @@ void control_task(void *param) {
|
||||
log = true;
|
||||
} 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;
|
||||
@@ -432,13 +441,9 @@ void control_task(void *param) {
|
||||
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)
|
||||
// Normal completion — deduct planned distance from leash
|
||||
remaining_distance -= this_move_dist;
|
||||
//else
|
||||
// remaining_distance -= distance_traveled;
|
||||
|
||||
current_state = STATE_DRIVE_END_DELAY;
|
||||
log = true;
|
||||
@@ -446,7 +451,7 @@ void control_task(void *param) {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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,38 +474,16 @@ 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;
|
||||
@@ -506,18 +491,16 @@ void control_task(void *param) {
|
||||
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;
|
||||
@@ -525,10 +508,8 @@ void control_task(void *param) {
|
||||
}
|
||||
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;
|
||||
@@ -536,10 +517,8 @@ void control_task(void *param) {
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user