Compare commits
5 Commits
fff1295862
...
cdb3b11db1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdb3b11db1 | ||
|
|
ff1ea6615c | ||
|
|
59e7071023 | ||
|
|
18faa5b83d | ||
|
|
35b7074e81 |
306
CLAUDE.md
306
CLAUDE.md
@@ -1,277 +1,61 @@
|
|||||||
# SC-F001 Firmware — CLAUDE.md
|
# SC-F001 Firmware — CLAUDE.md
|
||||||
|
|
||||||
## Overview
|
See `README.md` for full project documentation (hardware, architecture, protocols, algorithms).
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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:**
|
## sdkconfig Management
|
||||||
| 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):**
|
**Two files, different roles:**
|
||||||
- Port 0 (input): 2 physical buttons + 2 additional inputs
|
- `sdkconfig.defaults` — checked into git. Contains only intentional project overrides with comments explaining why. Applied by `idf.py reconfigure` on top of IDF defaults.
|
||||||
- Port 1 (output): 3× H-bridge relay pairs (DRIVE, JACK, AUX) + LEDs
|
- `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:**
|
**Rules:**
|
||||||
- `BRIDGE_DRIVE` — 100A max, ACS37220 sense chip (13.2 mV/A, inverted polarity)
|
- 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`.
|
||||||
- `BRIDGE_JACK` — 30A max, ACS37042 sense chip (44 mV/A)
|
- Never hand-edit `sdkconfig` without also updating `sdkconfig.defaults` for the same setting — otherwise the change will be lost on the next `reconfigure`.
|
||||||
- `BRIDGE_AUX` — 30A max, ACS37042 sense chip (44 mV/A)
|
- 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_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
|
## Managed Components
|
||||||
|
|
||||||
```
|
Only **mdns** is used. The TCA9555 is driven by a custom raw I2C driver in `i2c.c` (not the `esp-idf-lib/tca95x5` library). LittleFS is not used.
|
||||||
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):
|
`idf_component.yml` pins mdns to `~1.9.1` (compatible patch updates only). If adding a new component, pin it with `~` (e.g. `"~1.2.0"`) to allow patches but not breaking changes.
|
||||||
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:**
|
After changing `idf_component.yml`, run `idf.py reconfigure` to update `managed_components/`.
|
||||||
- FSM control task: priority 10 (real-time)
|
|
||||||
- All others: default priority
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Key Files
|
## Conventions
|
||||||
|
|
||||||
| File | Purpose |
|
- **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
|
||||||
| `main.c` | Entry point, 50ms main loop, factory reset, LED animation |
|
- **Concurrency:** FSM commands via `xQueueSend`; log writes via async queue; GPIO ISR → minimal work → sensor queue
|
||||||
| `control_fsm.c/h` | State machine, relay control, current monitoring, calibration |
|
- **State machine pattern:** transitions in one `switch`, relay outputs in a second `switch` (separated)
|
||||||
| `power_mgmt.c/h` | ADC reading, e-fuse thermal algorithm, battery voltage |
|
- **Watchdog:** `esp_task_wdt_add/reset` in each task, 10s timeout
|
||||||
| `sensors.c/h` | GPIO ISR-based sensor debouncing, encoder counters |
|
- **Logging:** `ESP_LOGI(TAG, ...)` per module; flash circular log for telemetry
|
||||||
| `i2c.c/h` | TCA9555 relay/LED/button control |
|
- **No dynamic allocation** in ISR or high-priority paths
|
||||||
| `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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -323,15 +107,3 @@ All fields optional. `parameters` is a flat object of param key → value.
|
|||||||
1. Add `<input id="PARAM_<KEY>" onchange="markChanged(this)"/>` in HTML
|
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
|
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
|
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
|
||||||
|
|||||||
113
TODO.md
113
TODO.md
@@ -1,55 +1,62 @@
|
|||||||
# SC-F001 Firmware — TODO
|
# SC-F001 Firmware — TODO
|
||||||
|
|
||||||
- [ ] sdkconfig audit
|
1. - [clauded] sdkconfig audit
|
||||||
- [ ] Enable `CONFIG_ESP_TASK_WDT_PANIC=y` (required for OTA rollback reset counter to work on WDT hangs)
|
- [clauded] Enable `CONFIG_ESP_TASK_WDT_PANIC=y` — added to sdkconfig.defaults and sdkconfig
|
||||||
- [ ] Verify `CONFIG_FREERTOS_CHECK_STACKOVERFLOW=2` is set (currently canary — confirmed)
|
- [clauded] Verify `CONFIG_FREERTOS_CHECK_STACKOVERFLOW=2` — confirmed canary method active
|
||||||
- [ ] Verify `CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT` is set (currently set — confirmed)
|
- [clauded] Verify `CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT` — confirmed active
|
||||||
- [ ] Confirm brownout detector level (~2.43V) is appropriate for 12V battery system with regulator
|
- [clauded] Confirm brownout detector level — ~2.43V is correct (ESP32 rail protection; battery low-V handled by FSM's `LOW_PROTECTION_V`)
|
||||||
- [ ] Research sdkconfig management best practices; document in CLAUDE.md
|
- [clauded] Research sdkconfig management best practices — documented in CLAUDE.md "sdkconfig Management" section
|
||||||
- [ ] Fix managed_components: remove unused deps, pin versions in `idf_component.yml`; document in CLAUDE.md
|
2. - [clauded] Fix managed_components: removed unused `littlefs` and `tca95x5` deps, pinned `mdns` to `~1.9.1`, bumped IDF min to `>=5.0`; documented in CLAUDE.md
|
||||||
- [ ] OTA rollback via consecutive-reset counter
|
3. - [clauded] OTA rollback via consecutive-reset counter
|
||||||
- [ ] Add `RTC_DATA_ATTR uint8_t reset_counter` — increment on boot, clear after successful health check
|
- [clauded] Add `RTC_DATA_ATTR uint8_t ota_reset_counter` — incremented on panic/WDT resets, cleared on power-on/ext reset
|
||||||
- [ ] On counter ≥ 5, call `esp_ota_mark_app_invalid_rollback_and_reboot()`
|
- [clauded] 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()`
|
- [clauded] After POST passes and FSM starts, call `esp_ota_mark_app_valid_cancel_rollback()` and clear counter
|
||||||
- [ ] Decide what "health check passes" means (POST passes? 30s uptime? first successful FSM cycle?)
|
- [clauded] Health check = POST passes + all critical inits + FSM task started + non-critical inits attempted
|
||||||
- [ ] Critical init failures (ADC, storage, log, I2C, FSM, sensors) should `esp_restart()` — this feeds the OTA rollback reset counter
|
4. - [clauded] Critical init failures (ADC, storage, log, I2C, FSM, UART) → `init_critical()` retries 3×, then `esp_restart()`
|
||||||
- [ ] Non-critical init failures (wifi, webserver, RF, BT) should log a `LOG_TYPE_ERROR` entry and attempt retry
|
5. - [clauded] Non-critical init failures (RF, BT, webserver) → log error, continue booting
|
||||||
- [ ] 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
|
- [clauded] WiFi/BT/RF retry once on init failure at boot (200ms delay for RF/BT, 500ms for WiFi), then log and continue
|
||||||
- [ ] Power-on self-test (POST) — run after all inits, before FSM starts; log results; feed OTA health check
|
6. - [clauded] Power-on self-test (POST) — `init_critical()` wrapper + dedicated POST checks after init
|
||||||
- [ ] ADC: read all 4 channels twice with short delay, flag if frozen or out of range (battery 5–25V, currents 0–150A)
|
- [clauded] ADC: `adc_post()` reads all 4 channels twice with 5ms delay, warns if frozen
|
||||||
- [ ] I2C: verify TCA9555 responds (read port 0)
|
- [clauded] I2C: `i2c_post()` verifies TCA9555 responds (read port 0)
|
||||||
- [ ] Flash: write-read-verify test on last sector of storage partition
|
- [clauded] Flash: `storage_post()` write-read-verify on last sector of storage partition
|
||||||
- [ ] Parameter validation
|
7. - [clauded] Parameter validation
|
||||||
- [ ] Add per-param bounds to `PARAM_LIST` macro (min, max, flags)
|
- [clauded] Add per-param bounds to `PARAM_LIST` macro (min, max) — extended PARAM_DEF 6-arg macro
|
||||||
- [ ] NaN/Inf → reset to default; out-of-range → clamp to min/max
|
- [clauded] NaN/Inf → reset to default; out-of-range → clamp to min/max — `validate_param()` in storage.c
|
||||||
- [ ] Enforce validation inside `commit_params()` (covers both `storage_init()` load and `/set` POST)
|
- [clauded] Enforce validation in `storage_init()` (after flash load) and `commit_params()` (before flash write)
|
||||||
- [ ] Audit for anywhere params are set without an immediate `commit_params()` call
|
- [clauded] Audit `set_param_value_t` calls outside comms.c — deleted dead code: `rf_433_set_keycode()`, `FSM_CMD_CALIBRATE_*_FINISH` handlers + FSM cases + `fsm_set_cal_val()` (web JS does cal math client-side, commits via standard param POST)
|
||||||
- [ ] Audit abandoned parameters (e.g. jack current) — add comments marking them deprecated
|
- [clauded] Audit abandoned parameters — `JACK_IS_DOWN` marked deprecated (may duplicate `JACK_I_DOWN`); `BOOT_TIME` is informational-only
|
||||||
- [ ] Factory reset: erase entire storage partition (not just params), require 10s button hold, LED indication (flash all → hold solid once triggered)
|
8. - [clauded] Factory reset: erases params + log + post_test partitions, requires 10s button hold on cold boot, LEDs flash during hold → solid when triggered
|
||||||
- [ ] Ensure RTC_DATA_ATTR variables survive panics/WDT resets
|
9. - [clauded] 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
|
- [clauded] Verified `sync_unix_us`, `sync_rtc_us`, `rtc_set` — no init path zeroes them; `rtc_restore_time()` recovers via RTC HW counter
|
||||||
- [ ] Verify `remaining_distance`, `fsm_error` (FSM state) are not zeroed except by intentional reset
|
- [clauded] Verified `remaining_distance`, `fsm_error` — `fsm_init()` does not touch them; only cleared by explicit user action
|
||||||
- [ ] Verify `log_head_offset`, `log_tail_offset` stay consistent after crash (no partial writes)
|
- [clauded] Verified `log_head_offset`, `log_tail_offset` — `log_init()` always recovers from flash scan; RTC_DATA_ATTR is historical/harmless
|
||||||
- [ ] Measure flash log write duration (bracket with `esp_timer_get_time()`, compare to WDT timeout of 5s)
|
10. - [clauded] Measure flash log write duration — `test_log_write_timing()` in log_test.c, runs 200 iterations of 39-byte writes, reports min/max/avg/sector-crossing times, compares to 5s WDT
|
||||||
- [ ] WiFi STA mode with event-group signaling
|
11. - [clauded] WiFi STA mode with event-group signaling
|
||||||
- [ ] Try connecting to saved STA network first, fall back to softAP on failure/timeout
|
- [clauded] STA-first with softAP fallback was already implemented in `start_wifi()`
|
||||||
- [ ] Add `EventGroupHandle_t` with `WIFI_READY_BIT` (set when STA connected or softAP up) and `BT_READY_BIT` (set when BT scan task starts)
|
- [clauded] Added `EventGroupHandle_t comms_event_group` in `comms_events.h` with `WIFI_READY_BIT` / `BT_READY_BIT`
|
||||||
- [ ] Replace blind 500ms `vTaskDelay` on alarm wake with `xEventGroupWaitBits()` + timeout
|
- [clauded] Replaced blind 500ms `vTaskDelay` on alarm wake with `xEventGroupWaitBits(COMMS_ALL_BITS, 5s timeout)`
|
||||||
- [ ] Use same event group in `soft_idle_exit()` path
|
- [clauded] `soft_idle_exit()` → `webserver_restart_wifi()` / `bt_hid_resume()` set bits; `webserver_stop()` / `bt_hid_stop()` clear bits
|
||||||
- [ ] Verify `sensors_init()` placement and ISR safety
|
- [clauded] Bits set even on permanent init failure so alarm-wake never blocks forever
|
||||||
- [ ] Confirm `sensors_init()` is safe to call from `app_main()` (research says yes — creates queue + installs ISR service, no task-context dependency)
|
12. - [clauded] Verify `sensors_init()` placement and ISR safety
|
||||||
- [ ] 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
|
- [clauded] Moved `sensors_init()` to main.c as `init_critical("SENSORS", sensors_init)` — runs before FSM
|
||||||
- [ ] Audit all ISRs are IRAM-safe: no `ESP_LOGx`, `printf`, `malloc`, or flash access — only `xQueueSendFromISR()`
|
- [clauded] Removed dead commented-out `sensors_init()` / `sensors_stop()` from sensors.c
|
||||||
- [ ] Handle `sensors_init()` failure as critical (→ reboot)
|
- [clauded] Audited ISR: `sensor_isr_handler` is IRAM_ATTR, uses only `esp_timer_get_time()` (IRAM-safe), `gpio_get_level()`, `xQueueSendFromISR()` — no logging/malloc/flash
|
||||||
- [ ] 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
|
- [clauded] `sensors_init()` failure is now critical (→ reboot via `init_critical`)
|
||||||
- [ ] Remove `rtc_wakeup_cause()` call (informational only, no longer needed)
|
13. - [clauded] External 32kHz crystal not needed (deep sleep disabled, soft idle instead) — removed crystal config from sdkconfig.defaults; `rtc_xtal_init()` already a no-op; crystal remains on PCB but unused
|
||||||
- [ ] Confirm `rtc_check_shutdown_timer()` uses signed subtraction — then remove the esp_timer overflow TODO comment (int64_t overflows after 292K years)
|
14. - [clauded] Removed `rtc_wakeup_cause()` — was unused (informational only, never called)
|
||||||
- [ ] Extract pure logic (e-fuse thermal model, param serialization, sensor debounce) into host-testable modules with Unity/CMock
|
15. - [clauded] Confirmed `rtc_check_shutdown_timer()` uses unsigned `TickType_t` subtraction — wraps correctly; removed esp_timer overflow TODO comment from main.c
|
||||||
- [ ] UART integration test framework: Python runner + ESP-side test commands
|
16. - [test] Logtool GUI output (matplotlib)
|
||||||
- [test] Logtool GUI output (matplotlib)
|
17. - [test] Verify naming convention adherence across codebase
|
||||||
- [test] Verify naming convention adherence across codebase
|
18. - [test] Verify WiFi SSID rename triggers comms reboot
|
||||||
- [test] Verify WiFi SSID rename triggers comms reboot
|
19. - [clauded] Documentation restructure
|
||||||
- [ ] Documentation restructure
|
- [clauded] Move project/hardware documentation from CLAUDE.md → README.md; keep CLAUDE.md for AI-specific instructions and conventions only
|
||||||
- [ ] 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
|
||||||
- [ ] 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")
|
||||||
- [ ] Add terse comments to FSM state transitions in `control_fsm.c` (focus on "why", not "what")
|
|
||||||
|
|
||||||
|
|
||||||
|
20. - [ ] Extract pure logic (e-fuse thermal model, param serialization, sensor debounce) into host-testable modules with Unity/CMock
|
||||||
|
21. - [ ] UART integration test framework: Python runner + ESP-side test commands
|
||||||
|
22. - [ ] Fix compile warnings
|
||||||
|
23. - [ ] Check if NVS needed for wifi/bluetooth (research first; what is it actually used for? can it be done without?)
|
||||||
|
24. - [ ] If NVS needed for wifi/bluetooth, compare its space efficiency, runtime efficiency, and security (errorchecking/crashes) to current params architecture
|
||||||
@@ -1,70 +1,4 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
esp-idf-lib/esp_idf_lib_helpers:
|
|
||||||
component_hash: 689853bb8993434f9556af0f2816e808bf77b5d22100144b21f3519993daf237
|
|
||||||
dependencies: []
|
|
||||||
source:
|
|
||||||
registry_url: https://components.espressif.com
|
|
||||||
type: service
|
|
||||||
targets:
|
|
||||||
- esp32
|
|
||||||
- esp32c2
|
|
||||||
- esp32c3
|
|
||||||
- esp32c5
|
|
||||||
- esp32c6
|
|
||||||
- esp32c61
|
|
||||||
- esp32h2
|
|
||||||
- esp32p4
|
|
||||||
- esp32s2
|
|
||||||
- esp32s3
|
|
||||||
version: 1.4.0
|
|
||||||
esp-idf-lib/i2cdev:
|
|
||||||
component_hash: 4f3838b2e68ab2b77fd43737139fa97dd0243b46af7b4a04588c67ff6b275ba1
|
|
||||||
dependencies:
|
|
||||||
- name: esp-idf-lib/esp_idf_lib_helpers
|
|
||||||
registry_url: https://components.espressif.com
|
|
||||||
require: private
|
|
||||||
version: '*'
|
|
||||||
source:
|
|
||||||
registry_url: https://components.espressif.com
|
|
||||||
type: service
|
|
||||||
targets:
|
|
||||||
- esp32
|
|
||||||
- esp32c2
|
|
||||||
- esp32c3
|
|
||||||
- esp32c5
|
|
||||||
- esp32c6
|
|
||||||
- esp32c61
|
|
||||||
- esp32h2
|
|
||||||
- esp32p4
|
|
||||||
- esp32s2
|
|
||||||
- esp32s3
|
|
||||||
version: 2.1.0
|
|
||||||
esp-idf-lib/tca95x5:
|
|
||||||
component_hash: 4bbdbd82828cf1fd5c03fd07e3ea2cb0f36daf16cb3ac7219d1e5decb9ec04ee
|
|
||||||
dependencies:
|
|
||||||
- name: esp-idf-lib/esp_idf_lib_helpers
|
|
||||||
registry_url: https://components.espressif.com
|
|
||||||
require: private
|
|
||||||
version: '*'
|
|
||||||
- name: esp-idf-lib/i2cdev
|
|
||||||
registry_url: https://components.espressif.com
|
|
||||||
require: private
|
|
||||||
version: '*'
|
|
||||||
source:
|
|
||||||
registry_url: https://components.espressif.com/
|
|
||||||
type: service
|
|
||||||
targets:
|
|
||||||
- esp32
|
|
||||||
- esp32c2
|
|
||||||
- esp32c3
|
|
||||||
- esp32c5
|
|
||||||
- esp32c6
|
|
||||||
- esp32c61
|
|
||||||
- esp32h2
|
|
||||||
- esp32p4
|
|
||||||
- esp32s2
|
|
||||||
- esp32s3
|
|
||||||
version: 1.0.7
|
|
||||||
espressif/mdns:
|
espressif/mdns:
|
||||||
component_hash: 29e47564b1a7ee778135e17fbbf2a2773f71c97ebabfe626c8eda7c958a7ad16
|
component_hash: 29e47564b1a7ee778135e17fbbf2a2773f71c97ebabfe626c8eda7c958a7ad16
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -79,21 +13,9 @@ dependencies:
|
|||||||
source:
|
source:
|
||||||
type: idf
|
type: idf
|
||||||
version: 5.3.1
|
version: 5.3.1
|
||||||
joltwallet/littlefs:
|
|
||||||
component_hash: 1808d73e99168f6f3c26dd31799a248484762b3a320ec4962dec11a145f4277f
|
|
||||||
dependencies:
|
|
||||||
- name: idf
|
|
||||||
require: private
|
|
||||||
version: '>=5.0'
|
|
||||||
source:
|
|
||||||
registry_url: https://components.espressif.com/
|
|
||||||
type: service
|
|
||||||
version: 1.20.3
|
|
||||||
direct_dependencies:
|
direct_dependencies:
|
||||||
- esp-idf-lib/tca95x5
|
|
||||||
- espressif/mdns
|
- espressif/mdns
|
||||||
- idf
|
- idf
|
||||||
- joltwallet/littlefs
|
manifest_hash: 7677ef9427111d5bfe7e9d00453defd2f35330f3a0aefe9690b0a5f577f93b06
|
||||||
manifest_hash: c3a20310a8ecc5e8e0221a7589abf8d2e372eb48f06d6b6fbb3fbf5f48a61aaf
|
|
||||||
target: esp32
|
target: esp32
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
include(${CMAKE_CURRENT_LIST_DIR}/version.cmake)
|
include(${CMAKE_CURRENT_LIST_DIR}/version.cmake)
|
||||||
|
|
||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS main.c log_test.c i2c.c rtc.c storage.c uart_comms.c control_fsm.c power_mgmt.c rf_433.c rtc.c sensors.c solar.c webserver.c simple_dns_server.c comms.c bt_hid.c # list the source files of this component
|
SRCS main.c log_test.c partition_test.c i2c.c rtc.c storage.c uart_comms.c control_fsm.c power_mgmt.c rf_433.c rtc.c sensors.c solar.c webserver.c simple_dns_server.c comms.c bt_hid.c # list the source files of this component
|
||||||
INCLUDE_DIRS "." "${CMAKE_BINARY_DIR}"
|
INCLUDE_DIRS "." "${CMAKE_BINARY_DIR}"
|
||||||
PRIV_INCLUDE_DIRS # optional, add here private include directories
|
PRIV_INCLUDE_DIRS # optional, add here private include directories
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
|
|
||||||
#include "bt_hid.h"
|
#include "bt_hid.h"
|
||||||
|
#include "comms_events.h"
|
||||||
#include "control_fsm.h"
|
#include "control_fsm.h"
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -581,6 +582,8 @@ esp_err_t bt_hid_init(void)
|
|||||||
*/
|
*/
|
||||||
xTaskCreate(bt_hid_scan_task, "bt_hid_scan", 6 * 1024, NULL, 4, &s_scan_task_handle);
|
xTaskCreate(bt_hid_scan_task, "bt_hid_scan", 6 * 1024, NULL, 4, &s_scan_task_handle);
|
||||||
|
|
||||||
|
if (comms_event_group) xEventGroupSetBits(comms_event_group, BT_READY_BIT);
|
||||||
|
|
||||||
ESP_LOGI(TAG, "BLE HID host initialised");
|
ESP_LOGI(TAG, "BLE HID host initialised");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
@@ -591,6 +594,7 @@ void bt_hid_stop(void)
|
|||||||
vTaskSuspend(s_scan_task_handle);
|
vTaskSuspend(s_scan_task_handle);
|
||||||
ESP_LOGI(TAG, "BT HID scan task suspended");
|
ESP_LOGI(TAG, "BT HID scan task suspended");
|
||||||
}
|
}
|
||||||
|
if (comms_event_group) xEventGroupClearBits(comms_event_group, BT_READY_BIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
void bt_hid_resume(void)
|
void bt_hid_resume(void)
|
||||||
@@ -599,4 +603,5 @@ void bt_hid_resume(void)
|
|||||||
vTaskResume(s_scan_task_handle);
|
vTaskResume(s_scan_task_handle);
|
||||||
ESP_LOGI(TAG, "BT HID scan task resumed");
|
ESP_LOGI(TAG, "BT HID scan task resumed");
|
||||||
}
|
}
|
||||||
|
if (comms_event_group) xEventGroupSetBits(comms_event_group, BT_READY_BIT);
|
||||||
}
|
}
|
||||||
|
|||||||
22
main/comms.c
22
main/comms.c
@@ -275,33 +275,11 @@ esp_err_t comms_handle_post(cJSON *root, cJSON **response_json) {
|
|||||||
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_PREP");
|
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_PREP");
|
||||||
cmd_executed = true;
|
cmd_executed = true;
|
||||||
}
|
}
|
||||||
else if (strcmp(cmd_str, "cal_jack_finish") == 0) {
|
|
||||||
cJSON *amt = cJSON_GetObjectItem(root, "amt");
|
|
||||||
if (cJSON_IsNumber(amt) && amt->valuedouble >= 0 && amt->valuedouble < 8) {
|
|
||||||
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_FINISH");
|
|
||||||
fsm_set_cal_val(amt->valuedouble);
|
|
||||||
fsm_request(FSM_CMD_CALIBRATE_JACK_FINISH);
|
|
||||||
cmd_executed = true;
|
|
||||||
} else {
|
|
||||||
error_msg = "cal_jack_finish requires amt parameter (0-8)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (strcmp(cmd_str, "cal_drive_start") == 0) {
|
else if (strcmp(cmd_str, "cal_drive_start") == 0) {
|
||||||
fsm_request(FSM_CMD_CALIBRATE_DRIVE_PREP);
|
fsm_request(FSM_CMD_CALIBRATE_DRIVE_PREP);
|
||||||
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_PREP");
|
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_PREP");
|
||||||
cmd_executed = true;
|
cmd_executed = true;
|
||||||
}
|
}
|
||||||
else if (strcmp(cmd_str, "cal_drive_finish") == 0) {
|
|
||||||
cJSON *amt = cJSON_GetObjectItem(root, "amt");
|
|
||||||
if (cJSON_IsNumber(amt) && amt->valuedouble >= 0 && amt->valuedouble < 8) {
|
|
||||||
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_FINISH");
|
|
||||||
fsm_set_cal_val(amt->valuedouble);
|
|
||||||
fsm_request(FSM_CMD_CALIBRATE_DRIVE_FINISH);
|
|
||||||
cmd_executed = true;
|
|
||||||
} else {
|
|
||||||
error_msg = "cal_drive_finish requires amt parameter (0-8)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (strcmp(cmd_str, "cal_get") == 0) {
|
else if (strcmp(cmd_str, "cal_get") == 0) {
|
||||||
ESP_LOGI(TAG, "CAL_GET");
|
ESP_LOGI(TAG, "CAL_GET");
|
||||||
|
|
||||||
|
|||||||
17
main/comms_events.h
Normal file
17
main/comms_events.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#ifndef COMMS_EVENTS_H
|
||||||
|
#define COMMS_EVENTS_H
|
||||||
|
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/event_groups.h"
|
||||||
|
|
||||||
|
// Shared event group for WiFi/BT readiness signaling.
|
||||||
|
// Set by webserver.c and bt_hid.c; waited on by main.c during alarm wake.
|
||||||
|
|
||||||
|
#define WIFI_READY_BIT BIT0 // Set when STA connected or softAP is up
|
||||||
|
#define BT_READY_BIT BIT1 // Set when BT scan task starts
|
||||||
|
#define COMMS_ALL_BITS (WIFI_READY_BIT | BT_READY_BIT)
|
||||||
|
|
||||||
|
// Must be created once (by main.c) before webserver_init() / bt_hid_init()
|
||||||
|
extern EventGroupHandle_t comms_event_group;
|
||||||
|
|
||||||
|
#endif // COMMS_EVENTS_H
|
||||||
@@ -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 "control_fsm.h"
|
||||||
#include "esp_task_wdt.h"
|
#include "esp_task_wdt.h"
|
||||||
@@ -32,6 +32,8 @@
|
|||||||
|
|
||||||
static QueueHandle_t fsm_cmd_queue = NULL;
|
static QueueHandle_t fsm_cmd_queue = NULL;
|
||||||
|
|
||||||
|
// AUDIT: fsm_init() does not zero these — they persist across panics/WDT resets.
|
||||||
|
// Only cleared by explicit user action (fsm_clear_error, fsm_set_remaining_distance).
|
||||||
RTC_DATA_ATTR esp_err_t fsm_error = ESP_OK;
|
RTC_DATA_ATTR esp_err_t fsm_error = ESP_OK;
|
||||||
esp_err_t fsm_get_error() { return fsm_error; }
|
esp_err_t fsm_get_error() { return fsm_error; }
|
||||||
void fsm_clear_error() { fsm_error = ESP_OK; }
|
void fsm_clear_error() { fsm_error = ESP_OK; }
|
||||||
@@ -39,7 +41,6 @@ void fsm_clear_error() { fsm_error = ESP_OK; }
|
|||||||
|
|
||||||
int64_t override_time = -1;
|
int64_t override_time = -1;
|
||||||
fsm_override_t override_cmd;
|
fsm_override_t override_cmd;
|
||||||
//int64_t override_cooldown[8] = {-1};
|
|
||||||
bool enabled = false;
|
bool enabled = false;
|
||||||
|
|
||||||
float this_move_dist = 0.0f;
|
float this_move_dist = 0.0f;
|
||||||
@@ -82,8 +83,6 @@ void pulse_override(fsm_override_t cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int64_t fsm_cal_t, fsm_cal_e;
|
int64_t fsm_cal_t, fsm_cal_e;
|
||||||
float fsm_cal_val;
|
|
||||||
void fsm_set_cal_val(float v) {fsm_cal_val = v;}
|
|
||||||
int64_t fsm_get_cal_t(){return fsm_cal_t;}
|
int64_t fsm_get_cal_t(){return fsm_cal_t;}
|
||||||
int64_t fsm_get_cal_e(){return fsm_cal_e;}
|
int64_t fsm_get_cal_e(){return fsm_cal_e;}
|
||||||
|
|
||||||
@@ -182,7 +181,7 @@ void control_task(void *param) {
|
|||||||
const TickType_t xFrequency = pdMS_TO_TICKS(20);
|
const TickType_t xFrequency = pdMS_TO_TICKS(20);
|
||||||
enabled = true;
|
enabled = true;
|
||||||
|
|
||||||
sensors_init(); // TODO: Why is this *here* rather than in main?
|
// sensors_init() is called from main.c as a critical init (before FSM starts)
|
||||||
|
|
||||||
while (enabled) {
|
while (enabled) {
|
||||||
vTaskDelayUntil(&xLastWakeTime, xFrequency);
|
vTaskDelayUntil(&xLastWakeTime, xFrequency);
|
||||||
@@ -289,14 +288,6 @@ void control_task(void *param) {
|
|||||||
log = true;
|
log = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case FSM_CMD_CALIBRATE_JACK_FINISH:
|
|
||||||
set_param_value_t(PARAM_JACK_KT,
|
|
||||||
(param_value_t){.f32 = fsm_cal_t / fsm_cal_val});
|
|
||||||
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_FINISH -> %f", get_param_value_t(PARAM_JACK_KT).f32);
|
|
||||||
break;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
case FSM_CMD_CALIBRATE_DRIVE_PREP:
|
case FSM_CMD_CALIBRATE_DRIVE_PREP:
|
||||||
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_PREP");
|
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_PREP");
|
||||||
if (current_state == STATE_IDLE
|
if (current_state == STATE_IDLE
|
||||||
@@ -325,43 +316,39 @@ void control_task(void *param) {
|
|||||||
log = true;
|
log = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case FSM_CMD_CALIBRATE_DRIVE_FINISH:
|
|
||||||
set_param_value_t(PARAM_DRIVE_KT,
|
|
||||||
(param_value_t){.f32 = fsm_cal_t / fsm_cal_val});
|
|
||||||
set_param_value_t(PARAM_DRIVE_KE,
|
|
||||||
(param_value_t){.f32 = fsm_cal_e / fsm_cal_val});
|
|
||||||
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_FINISH -> %f / %f",
|
|
||||||
get_param_value_t(PARAM_DRIVE_KT).f32,
|
|
||||||
get_param_value_t(PARAM_DRIVE_KE).f32);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!enabled) break;
|
if (!enabled) break;
|
||||||
|
|
||||||
/**** STATE TRANSITIONS ****/
|
/**** 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) {
|
switch (current_state) {
|
||||||
case STATE_IDLE:
|
case STATE_IDLE:
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case STATE_MOVE_START_DELAY:
|
case STATE_MOVE_START_DELAY:
|
||||||
|
// 1s pause before raising jack — lets operator abort after pressing start
|
||||||
if (!get_is_safe()) {
|
if (!get_is_safe()) {
|
||||||
fsm_error = SC_ERR_SAFETY_TRIP;
|
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;
|
log = true;
|
||||||
} else if (timer_done()) {
|
} else if (timer_done()) {
|
||||||
current_state = STATE_JACK_UP_START;
|
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;
|
jack_start_us = fsm_now;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case STATE_JACK_UP_START:
|
case STATE_JACK_UP_START:
|
||||||
|
// Detect when jack engages the load (current spike, efuse, or timeout)
|
||||||
if (!get_is_safe()) {
|
if (!get_is_safe()) {
|
||||||
fsm_error = SC_ERR_SAFETY_TRIP;
|
fsm_error = SC_ERR_SAFETY_TRIP;
|
||||||
current_state = STATE_UNDO_JACK_START;
|
current_state = STATE_UNDO_JACK_START;
|
||||||
jack_finish_us = fsm_now;
|
jack_finish_us = fsm_now;
|
||||||
log = true;
|
log = true;
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if (efuse_get(BRIDGE_JACK)) {
|
if (efuse_get(BRIDGE_JACK)) {
|
||||||
ESP_LOGI(TAG, "START->UP BY EFUSE");
|
ESP_LOGI(TAG, "START->UP BY EFUSE");
|
||||||
current_state = STATE_JACK_UP;
|
current_state = STATE_JACK_UP;
|
||||||
@@ -387,7 +374,9 @@ void control_task(void *param) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case STATE_JACK_UP:
|
case STATE_JACK_UP:
|
||||||
|
// Continue raising until timer or efuse — records finish time for symmetric jack-down
|
||||||
if (!get_is_safe()) {
|
if (!get_is_safe()) {
|
||||||
fsm_error = SC_ERR_SAFETY_TRIP;
|
fsm_error = SC_ERR_SAFETY_TRIP;
|
||||||
current_state = STATE_UNDO_JACK_START;
|
current_state = STATE_UNDO_JACK_START;
|
||||||
@@ -396,15 +385,16 @@ void control_task(void *param) {
|
|||||||
log = true;
|
log = true;
|
||||||
} else {
|
} else {
|
||||||
if (timer_done() || efuse_get(BRIDGE_JACK)) {
|
if (timer_done() || efuse_get(BRIDGE_JACK)) {
|
||||||
// Track total time including first phase
|
|
||||||
current_state = STATE_DRIVE_START_DELAY;
|
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;
|
log = true;
|
||||||
set_timer(TRANSITION_DELAY_US);
|
set_timer(TRANSITION_DELAY_US);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case STATE_DRIVE_START_DELAY:
|
case STATE_DRIVE_START_DELAY:
|
||||||
|
// 1s pause between jack-up and drive — mechanical settling
|
||||||
if (!get_is_safe()) {
|
if (!get_is_safe()) {
|
||||||
fsm_error = SC_ERR_SAFETY_TRIP;
|
fsm_error = SC_ERR_SAFETY_TRIP;
|
||||||
current_state = STATE_UNDO_JACK_START;
|
current_state = STATE_UNDO_JACK_START;
|
||||||
@@ -414,13 +404,14 @@ void control_task(void *param) {
|
|||||||
current_state = STATE_DRIVE;
|
current_state = STATE_DRIVE;
|
||||||
log = true;
|
log = true;
|
||||||
set_timer(DRIVE_TIME);
|
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);
|
set_sensor_counter(SENSOR_DRIVE, -DRIVE_DIST);
|
||||||
// Record starting encoder position AFTER setting it
|
|
||||||
move_start_encoder = get_sensor_counter(SENSOR_DRIVE);
|
move_start_encoder = get_sensor_counter(SENSOR_DRIVE);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case STATE_DRIVE:
|
case STATE_DRIVE:
|
||||||
|
// Horizontal travel — stops on timer, encoder target, or efuse trip
|
||||||
if (!get_is_safe()) {
|
if (!get_is_safe()) {
|
||||||
fsm_error = SC_ERR_SAFETY_TRIP;
|
fsm_error = SC_ERR_SAFETY_TRIP;
|
||||||
current_state = STATE_UNDO_JACK_START;
|
current_state = STATE_UNDO_JACK_START;
|
||||||
@@ -432,13 +423,9 @@ void control_task(void *param) {
|
|||||||
float ke = get_param_value_t(PARAM_DRIVE_KE).f32;
|
float ke = get_param_value_t(PARAM_DRIVE_KE).f32;
|
||||||
float distance_traveled = ticks_traveled / ke;
|
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) {
|
if (timer_done() || current_encoder > 0) {
|
||||||
// Update remaining distance based on actual travel
|
// Normal completion — deduct planned distance from leash
|
||||||
//if (current_encoder < 0)
|
|
||||||
remaining_distance -= this_move_dist;
|
remaining_distance -= this_move_dist;
|
||||||
//else
|
|
||||||
// remaining_distance -= distance_traveled;
|
|
||||||
|
|
||||||
current_state = STATE_DRIVE_END_DELAY;
|
current_state = STATE_DRIVE_END_DELAY;
|
||||||
log = true;
|
log = true;
|
||||||
@@ -446,7 +433,7 @@ void control_task(void *param) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (efuse_get(BRIDGE_DRIVE)) {
|
if (efuse_get(BRIDGE_DRIVE)) {
|
||||||
// Update remaining distance even on fault
|
// Fault — deduct actual distance traveled (may be partial)
|
||||||
remaining_distance -= distance_traveled;
|
remaining_distance -= distance_traveled;
|
||||||
if (remaining_distance < 0.0f) remaining_distance = 0.0f;
|
if (remaining_distance < 0.0f) remaining_distance = 0.0f;
|
||||||
|
|
||||||
@@ -457,7 +444,9 @@ void control_task(void *param) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case STATE_DRIVE_END_DELAY:
|
case STATE_DRIVE_END_DELAY:
|
||||||
|
// 1s pause after drive — then lower jack
|
||||||
if (!get_is_safe()) {
|
if (!get_is_safe()) {
|
||||||
fsm_error = SC_ERR_SAFETY_TRIP;
|
fsm_error = SC_ERR_SAFETY_TRIP;
|
||||||
current_state = STATE_UNDO_JACK_START;
|
current_state = STATE_UNDO_JACK_START;
|
||||||
@@ -467,38 +456,16 @@ void control_task(void *param) {
|
|||||||
log = true;
|
log = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case STATE_JACK_DOWN:
|
case STATE_JACK_DOWN:
|
||||||
|
// Lower jack — stops on efuse (hit ground), position sensor, or timeout
|
||||||
if (efuse_get(BRIDGE_JACK)) {
|
if (efuse_get(BRIDGE_JACK)) {
|
||||||
|
|
||||||
ESP_LOGI(TAG, "DOWN->IDLE BY EFUSE");
|
ESP_LOGI(TAG, "DOWN->IDLE BY EFUSE");
|
||||||
// Current spike detected
|
|
||||||
current_state = STATE_IDLE;
|
current_state = STATE_IDLE;
|
||||||
log = true;
|
log = true;
|
||||||
break;
|
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)) {
|
if (get_sensor(SENSOR_JACK)) {
|
||||||
ESP_LOGI(TAG, "DOWN->IDLE BY SENSOR");
|
ESP_LOGI(TAG, "DOWN->IDLE BY SENSOR");
|
||||||
current_state = STATE_IDLE;
|
current_state = STATE_IDLE;
|
||||||
@@ -506,18 +473,16 @@ void control_task(void *param) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timer_done() ) {
|
if (timer_done()) {
|
||||||
ESP_LOGI(TAG, "DOWN->IDLE BY TIME");
|
ESP_LOGI(TAG, "DOWN->IDLE BY TIME");
|
||||||
current_state = STATE_IDLE;
|
current_state = STATE_IDLE;
|
||||||
log = true;
|
log = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
case STATE_UNDO_JACK_START:
|
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)) {
|
if (!efuse_get(BRIDGE_JACK)) {
|
||||||
set_timer(JACK_DOWN_TIME);
|
set_timer(JACK_DOWN_TIME);
|
||||||
current_state = STATE_JACK_DOWN;
|
current_state = STATE_JACK_DOWN;
|
||||||
@@ -525,10 +490,8 @@ void control_task(void *param) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
case STATE_CALIBRATE_JACK_DELAY:
|
case STATE_CALIBRATE_JACK_DELAY:
|
||||||
// no way out of this except a command
|
break; // waiting for user command to begin measurement
|
||||||
break;
|
|
||||||
case STATE_CALIBRATE_JACK_MOVE:
|
case STATE_CALIBRATE_JACK_MOVE:
|
||||||
if (timer_done()) {
|
if (timer_done()) {
|
||||||
current_state = STATE_IDLE;
|
current_state = STATE_IDLE;
|
||||||
@@ -536,10 +499,8 @@ void control_task(void *param) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
case STATE_CALIBRATE_DRIVE_DELAY:
|
case STATE_CALIBRATE_DRIVE_DELAY:
|
||||||
// no way out of this except a command
|
break; // waiting for user command to begin measurement
|
||||||
break;
|
|
||||||
case STATE_CALIBRATE_DRIVE_MOVE:
|
case STATE_CALIBRATE_DRIVE_MOVE:
|
||||||
if (!get_is_safe() || timer_done()) {
|
if (!get_is_safe() || timer_done()) {
|
||||||
current_state = STATE_IDLE;
|
current_state = STATE_IDLE;
|
||||||
|
|||||||
@@ -17,12 +17,10 @@ typedef enum {
|
|||||||
FSM_CMD_CALIBRATE_JACK_PREP,
|
FSM_CMD_CALIBRATE_JACK_PREP,
|
||||||
FSM_CMD_CALIBRATE_JACK_START,
|
FSM_CMD_CALIBRATE_JACK_START,
|
||||||
FSM_CMD_CALIBRATE_JACK_END,
|
FSM_CMD_CALIBRATE_JACK_END,
|
||||||
FSM_CMD_CALIBRATE_JACK_FINISH,
|
|
||||||
|
|
||||||
FSM_CMD_CALIBRATE_DRIVE_PREP,
|
FSM_CMD_CALIBRATE_DRIVE_PREP,
|
||||||
FSM_CMD_CALIBRATE_DRIVE_START,
|
FSM_CMD_CALIBRATE_DRIVE_START,
|
||||||
FSM_CMD_CALIBRATE_DRIVE_END,
|
FSM_CMD_CALIBRATE_DRIVE_END
|
||||||
FSM_CMD_CALIBRATE_DRIVE_FINISH
|
|
||||||
} fsm_cmd_t;
|
} fsm_cmd_t;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@@ -90,7 +88,6 @@ void pulse_override(fsm_override_t cmd);
|
|||||||
esp_err_t fsm_init();
|
esp_err_t fsm_init();
|
||||||
esp_err_t fsm_stop();
|
esp_err_t fsm_stop();
|
||||||
|
|
||||||
void fsm_set_cal_val(float v);
|
|
||||||
int64_t fsm_get_cal_t();
|
int64_t fsm_get_cal_t();
|
||||||
int64_t fsm_get_cal_e();
|
int64_t fsm_get_cal_e();
|
||||||
void fsm_request(fsm_cmd_t cmd);
|
void fsm_request(fsm_cmd_t cmd);
|
||||||
|
|||||||
12
main/i2c.c
12
main/i2c.c
@@ -52,6 +52,18 @@ esp_err_t i2c_init(void) {
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
esp_err_t i2c_post(void) {
|
||||||
|
// Verify TCA9555 responds by reading input port 0
|
||||||
|
uint16_t val = 0;
|
||||||
|
esp_err_t err = tca_read_word(TCA_REG_INPUT0, &val);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE("I2C", "POST: TCA9555 read failed: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
ESP_LOGI("I2C", "POST: TCA9555 OK (port0=0x%04X)", val);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
esp_err_t i2c_set_relays(relay_port_t states) {
|
esp_err_t i2c_set_relays(relay_port_t states) {
|
||||||
return tca_write_word_8(TCA_REG_OUTPUT1, states.raw);
|
return tca_write_word_8(TCA_REG_OUTPUT1, states.raw);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ typedef union {
|
|||||||
|
|
||||||
// Public Functions
|
// Public Functions
|
||||||
esp_err_t i2c_init(void);
|
esp_err_t i2c_init(void);
|
||||||
|
esp_err_t i2c_post(void);
|
||||||
esp_err_t i2c_stop(void);
|
esp_err_t i2c_stop(void);
|
||||||
|
|
||||||
esp_err_t i2c_set_relays(relay_port_t states);
|
esp_err_t i2c_set_relays(relay_port_t states);
|
||||||
|
|||||||
@@ -1,19 +1,6 @@
|
|||||||
## IDF Component Manager Manifest File
|
## IDF Component Manager Manifest File
|
||||||
dependencies:
|
dependencies:
|
||||||
espressif/mdns: "*"
|
espressif/mdns: "~1.9.1"
|
||||||
joltwallet/littlefs: "==1.20.3"
|
|
||||||
esp-idf-lib/tca95x5: "*"
|
|
||||||
## Required IDF version
|
## Required IDF version
|
||||||
idf:
|
idf:
|
||||||
version: ">=4.1.0"
|
version: ">=5.0"
|
||||||
# # Put list of dependencies here
|
|
||||||
# # For components maintained by Espressif:
|
|
||||||
# component: "~1.0.0"
|
|
||||||
# # For 3rd party components:
|
|
||||||
# username/component: ">=1.0.0,<2.0.0"
|
|
||||||
# username2/component2:
|
|
||||||
# version: "~1.0.0"
|
|
||||||
# # For transient dependencies `public` flag can be set.
|
|
||||||
# # `public` flag doesn't have an effect dependencies of the `main` component.
|
|
||||||
# # All dependencies of `main` are public by default.
|
|
||||||
# public: true
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "esp_task_wdt.h"
|
#include "esp_task_wdt.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
@@ -1087,6 +1088,87 @@ int count_passed_tests(test_result_t* results, int num_tests) {
|
|||||||
return passed;
|
return passed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Write timing benchmark — measures blocking write duration
|
||||||
|
// ============================================================================
|
||||||
|
void test_log_write_timing(void) {
|
||||||
|
ESP_LOGI(TAG, "");
|
||||||
|
ESP_LOGI(TAG, "=== Log Write Timing Benchmark ===");
|
||||||
|
|
||||||
|
// Erase and reinit to get a clean state
|
||||||
|
esp_err_t err = log_erase_all_sectors();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to erase log for timing test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
|
||||||
|
// Use a 39-byte payload (typical FSM log entry size)
|
||||||
|
uint8_t payload[39];
|
||||||
|
for (int i = 0; i < 39; i++) payload[i] = (uint8_t)i;
|
||||||
|
|
||||||
|
#define TIMING_ITERATIONS 200
|
||||||
|
|
||||||
|
int64_t min_us = INT64_MAX;
|
||||||
|
int64_t max_us = 0;
|
||||||
|
int64_t total_us = 0;
|
||||||
|
int sector_cross_count = 0;
|
||||||
|
int64_t sector_cross_max_us = 0;
|
||||||
|
|
||||||
|
uint32_t prev_head = log_get_head();
|
||||||
|
|
||||||
|
for (int i = 0; i < TIMING_ITERATIONS; i++) {
|
||||||
|
int64_t t0 = esp_timer_get_time();
|
||||||
|
err = log_write_blocking_test(payload, sizeof(payload), LOG_TYPE_DATA);
|
||||||
|
// Wait for queue flush
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(50));
|
||||||
|
int64_t t1 = esp_timer_get_time();
|
||||||
|
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Write %d failed: %s", i, esp_err_to_name(err));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t dt = t1 - t0;
|
||||||
|
total_us += dt;
|
||||||
|
if (dt < min_us) min_us = dt;
|
||||||
|
if (dt > max_us) max_us = dt;
|
||||||
|
|
||||||
|
// Detect sector crossing (head wrapped or jumped by > payload size)
|
||||||
|
uint32_t cur_head = log_get_head();
|
||||||
|
if (cur_head < prev_head || (cur_head - prev_head) > sizeof(payload) + 10) {
|
||||||
|
sector_cross_count++;
|
||||||
|
if (dt > sector_cross_max_us) sector_cross_max_us = dt;
|
||||||
|
}
|
||||||
|
prev_head = cur_head;
|
||||||
|
|
||||||
|
if (i % 50 == 0) esp_task_wdt_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t avg_us = total_us / TIMING_ITERATIONS;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "");
|
||||||
|
ESP_LOGI(TAG, "=== WRITE TIMING REPORT ===");
|
||||||
|
ESP_LOGI(TAG, " Iterations: %d", TIMING_ITERATIONS);
|
||||||
|
ESP_LOGI(TAG, " Payload size: %d bytes", (int)sizeof(payload));
|
||||||
|
ESP_LOGI(TAG, " Min: %lld us", (long long)min_us);
|
||||||
|
ESP_LOGI(TAG, " Max: %lld us", (long long)max_us);
|
||||||
|
ESP_LOGI(TAG, " Avg: %lld us", (long long)avg_us);
|
||||||
|
ESP_LOGI(TAG, " Sector crossings: %d (max %lld us)", sector_cross_count, (long long)sector_cross_max_us);
|
||||||
|
ESP_LOGI(TAG, " WDT margin: %.1fs (WDT=5s, worst=%lldus)",
|
||||||
|
5.0 - (double)max_us / 1000000.0, (long long)max_us);
|
||||||
|
if (max_us > 1000000) {
|
||||||
|
ESP_LOGW(TAG, " WARNING: max write > 1s — close to WDT timeout!");
|
||||||
|
} else if (max_us > 100000) {
|
||||||
|
ESP_LOGI(TAG, " Note: max write > 100ms (expected during sector erase)");
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "===========================");
|
||||||
|
ESP_LOGI(TAG, "");
|
||||||
|
|
||||||
|
#undef TIMING_ITERATIONS
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
}
|
||||||
|
|
||||||
// Main test runner
|
// Main test runner
|
||||||
esp_err_t run_all_log_tests(void) {
|
esp_err_t run_all_log_tests(void) {
|
||||||
ESP_LOGI(TAG, "\n\n");
|
ESP_LOGI(TAG, "\n\n");
|
||||||
@@ -1169,10 +1251,10 @@ esp_err_t run_all_log_tests(void) {
|
|||||||
|
|
||||||
if (passed == num_tests) {
|
if (passed == num_tests) {
|
||||||
ESP_LOGI(TAG, "ALL TESTS PASSED!");
|
ESP_LOGI(TAG, "ALL TESTS PASSED!");
|
||||||
|
// Run write timing benchmark as a final report (not a pass/fail test)
|
||||||
|
test_log_write_timing();
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGE(TAG, "SOME TESTS FAILED!");
|
ESP_LOGE(TAG, "SOME TESTS FAILED!");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
while(1) { esp_task_wdt_reset(); vTaskDelay(pdMS_TO_TICKS(100)); }
|
while(1) { esp_task_wdt_reset(); vTaskDelay(pdMS_TO_TICKS(100)); }
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ bool test_log_full_partition(void);
|
|||||||
bool test_log_read_after_write(void);
|
bool test_log_read_after_write(void);
|
||||||
bool test_log_multiple_types(void);
|
bool test_log_multiple_types(void);
|
||||||
|
|
||||||
|
// Write timing benchmark (not a pass/fail test — prints min/max/avg report)
|
||||||
|
void test_log_write_timing(void);
|
||||||
|
|
||||||
// Helper functions for testing
|
// Helper functions for testing
|
||||||
void print_test_results(test_result_t* results, int num_tests);
|
void print_test_results(test_result_t* results, int num_tests);
|
||||||
int count_passed_tests(test_result_t* results, int num_tests);
|
int count_passed_tests(test_result_t* results, int num_tests);
|
||||||
|
|||||||
176
main/main.c
176
main/main.c
@@ -1,7 +1,9 @@
|
|||||||
#include "esp_task_wdt.h"
|
#include "esp_task_wdt.h"
|
||||||
#include "esp_system.h"
|
#include "esp_system.h"
|
||||||
|
#include "esp_ota_ops.h"
|
||||||
#include "i2c.h"
|
#include "i2c.h"
|
||||||
#include "log_test.h"
|
#include "log_test.h"
|
||||||
|
#include "partition_test.h"
|
||||||
#include "storage.h"
|
#include "storage.h"
|
||||||
#include "uart_comms.h"
|
#include "uart_comms.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
@@ -15,11 +17,36 @@
|
|||||||
#include "rf_433.h"
|
#include "rf_433.h"
|
||||||
#include "bt_hid.h"
|
#include "bt_hid.h"
|
||||||
#include "webserver.h"
|
#include "webserver.h"
|
||||||
|
#include "comms_events.h"
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
EventGroupHandle_t comms_event_group = NULL;
|
||||||
|
|
||||||
#define TAG "MAIN"
|
#define TAG "MAIN"
|
||||||
|
|
||||||
|
#define POST_MAX_RETRIES 3
|
||||||
|
#define OTA_ROLLBACK_THRESHOLD 5
|
||||||
|
#define FACTORY_RESET_HOLD_MS 10000
|
||||||
|
|
||||||
|
// Survives resets (panic, WDT, sw reset) but NOT power-on or external reset
|
||||||
|
RTC_DATA_ATTR static uint8_t ota_reset_counter = 0;
|
||||||
|
|
||||||
|
// Try an init function up to POST_MAX_RETRIES times. On final failure, reboot.
|
||||||
|
// Critical inits (ADC, I2C, storage, FSM, sensors) use this — a permanent failure
|
||||||
|
// feeds the OTA rollback reset counter via the panic→reboot path.
|
||||||
|
static void init_critical(const char *name, esp_err_t (*fn)(void)) {
|
||||||
|
for (int attempt = 1; attempt <= POST_MAX_RETRIES; attempt++) {
|
||||||
|
esp_err_t err = fn();
|
||||||
|
if (err == ESP_OK) return;
|
||||||
|
ESP_LOGE(TAG, "%s FAILED (attempt %d/%d): %s", name, attempt, POST_MAX_RETRIES, esp_err_to_name(err));
|
||||||
|
if (attempt < POST_MAX_RETRIES) vTaskDelay(pdMS_TO_TICKS(100));
|
||||||
|
}
|
||||||
|
ESP_LOGE(TAG, "%s FAILED after %d attempts — rebooting", name, POST_MAX_RETRIES);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
esp_restart();
|
||||||
|
}
|
||||||
|
|
||||||
int64_t last_bat_log_time = 0;
|
int64_t last_bat_log_time = 0;
|
||||||
esp_err_t send_bat_log() {
|
esp_err_t send_bat_log() {
|
||||||
if(!rtc_is_set()) return ESP_OK;
|
if(!rtc_is_set()) return ESP_OK;
|
||||||
@@ -103,50 +130,53 @@ void drive_leds(led_state_t state) {
|
|||||||
|
|
||||||
void app_main(void) {esp_task_wdt_add(NULL);
|
void app_main(void) {esp_task_wdt_add(NULL);
|
||||||
|
|
||||||
//run_all_log_tests();
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Firmware: %s", FIRMWARE_STRING);
|
ESP_LOGI(TAG, "Firmware: %s", FIRMWARE_STRING);
|
||||||
ESP_LOGI(TAG, "Version: %s", FIRMWARE_VERSION);
|
ESP_LOGI(TAG, "Version: %s", FIRMWARE_VERSION);
|
||||||
ESP_LOGI(TAG, "Branch: %s", FIRMWARE_BRANCH);
|
ESP_LOGI(TAG, "Branch: %s", FIRMWARE_BRANCH);
|
||||||
ESP_LOGI(TAG, "Built: %s", BUILD_DATE);
|
ESP_LOGI(TAG, "Built: %s", BUILD_DATE);
|
||||||
|
|
||||||
// TODO: Check wdt stuff
|
// TODO: Confirm whether external RTC crystal can be dropped (see TODO.md #13)
|
||||||
// TODO: Stack Overflow Detection
|
|
||||||
// TODO: Remove XTAL crystal stuff
|
|
||||||
if (rtc_xtal_init() != ESP_OK) ESP_LOGE(TAG, "RTC FAILED");
|
if (rtc_xtal_init() != ESP_OK) ESP_LOGE(TAG, "RTC FAILED");
|
||||||
rtc_restore_time(); // Recover time from RTC domain if we crashed
|
rtc_restore_time(); // Recover time from RTC domain if we crashed
|
||||||
|
|
||||||
// Say hello; turn on the lights
|
// Critical inits — retry up to 3 times, then reboot (feeds OTA rollback counter)
|
||||||
rtc_wakeup_cause(); // log wakeup cause (informational only) // TODO: Shouldnt be needed anymore
|
init_critical("I2C", i2c_init);
|
||||||
if (i2c_init() != ESP_OK) ESP_LOGE(TAG, "I2C FAILED");
|
i2c_post(); // verify TCA9555 responds
|
||||||
i2c_set_relays((relay_port_t){.raw=0});
|
i2c_set_relays((relay_port_t){.raw=0});
|
||||||
drive_leds(LED_STATE_BOOTING);
|
drive_leds(LED_STATE_BOOTING);
|
||||||
|
|
||||||
// TODO: How many tasks do we have?
|
|
||||||
|
|
||||||
|
// Factory reset: cold boot + button held for 10s
|
||||||
// Check for factory reset condition: Cold boot (power-on/ext-reset) + button held
|
// LEDs flash while waiting, go solid when triggered
|
||||||
esp_reset_reason_t boot_reset_reason = esp_reset_reason();
|
esp_reset_reason_t boot_reset_reason = esp_reset_reason();
|
||||||
if ((boot_reset_reason == ESP_RST_POWERON || boot_reset_reason == ESP_RST_EXT)
|
if ((boot_reset_reason == ESP_RST_POWERON || boot_reset_reason == ESP_RST_EXT)
|
||||||
&& gpio_get_level(GPIO_NUM_13) == 0) {
|
&& gpio_get_level(GPIO_NUM_13) == 0) {
|
||||||
ESP_LOGW(TAG, "FACTORY RESET TRIGGERED - Button held on cold boot");
|
ESP_LOGW(TAG, "Button held on cold boot — hold %ds for factory reset", FACTORY_RESET_HOLD_MS / 1000);
|
||||||
|
|
||||||
// Flash LED pattern to indicate factory reset
|
// Flash all LEDs while user holds button (100ms on/off cycle)
|
||||||
for (int i = 0; i < 10; i++) {
|
int held_ms = 0;
|
||||||
i2c_set_led1(0b111);
|
while (gpio_get_level(GPIO_NUM_13) == 0 && held_ms < FACTORY_RESET_HOLD_MS) {
|
||||||
vTaskDelay(pdMS_TO_TICKS(100));
|
i2c_set_led1((held_ms / 100) % 2 ? 0b111 : 0b000);
|
||||||
i2c_set_led1(0b000);
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(100));
|
vTaskDelay(pdMS_TO_TICKS(100));
|
||||||
|
held_ms += 100;
|
||||||
|
esp_task_wdt_reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize minimal components needed for factory reset
|
if (held_ms < FACTORY_RESET_HOLD_MS) {
|
||||||
|
ESP_LOGI(TAG, "Button released early (%dms) — skipping factory reset", held_ms);
|
||||||
|
i2c_set_led1(0b000);
|
||||||
|
} else {
|
||||||
|
// Solid LEDs = reset triggered
|
||||||
|
i2c_set_led1(0b111);
|
||||||
|
ESP_LOGW(TAG, "FACTORY RESET TRIGGERED");
|
||||||
|
|
||||||
|
// Initialize storage so we can erase it
|
||||||
if (storage_init() != ESP_OK) ESP_LOGE(TAG, "STORAGE FAILED");
|
if (storage_init() != ESP_OK) ESP_LOGE(TAG, "STORAGE FAILED");
|
||||||
|
|
||||||
// Perform factory reset
|
|
||||||
esp_err_t reset_err = factory_reset();
|
esp_err_t reset_err = factory_reset();
|
||||||
if (reset_err == ESP_OK) {
|
if (reset_err == ESP_OK) {
|
||||||
ESP_LOGI(TAG, "Factory reset completed successfully");
|
ESP_LOGI(TAG, "Factory reset completed successfully");
|
||||||
// Flash success pattern
|
// Success: green blink
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
i2c_set_led1(0b010);
|
i2c_set_led1(0b010);
|
||||||
vTaskDelay(pdMS_TO_TICKS(200));
|
vTaskDelay(pdMS_TO_TICKS(200));
|
||||||
@@ -155,7 +185,7 @@ void app_main(void) {esp_task_wdt_add(NULL);
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGE(TAG, "Factory reset failed!");
|
ESP_LOGE(TAG, "Factory reset failed!");
|
||||||
// Flash error pattern
|
// Error: red blink
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
i2c_set_led1(0b100);
|
i2c_set_led1(0b100);
|
||||||
vTaskDelay(pdMS_TO_TICKS(200));
|
vTaskDelay(pdMS_TO_TICKS(200));
|
||||||
@@ -164,18 +194,22 @@ void app_main(void) {esp_task_wdt_add(NULL);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reboot the system
|
|
||||||
ESP_LOGI(TAG, "Rebooting system...");
|
ESP_LOGI(TAG, "Rebooting system...");
|
||||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
esp_restart();
|
esp_restart();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Every boot we load parameters and monitor solar, no matter what
|
// Critical inits — retry up to 3 times, then reboot
|
||||||
// TODO: Do things with errors (put in real log? then reset. "assert with LOGE"?)
|
init_critical("ADC", adc_init);
|
||||||
if (adc_init() != ESP_OK) ESP_LOGE(TAG, "ADC FAILED");
|
init_critical("STORAGE", storage_init);
|
||||||
if (storage_init() != ESP_OK) ESP_LOGE(TAG, "STORAGE FAILED");
|
init_critical("LOG", log_init);
|
||||||
if (log_init() != ESP_OK) ESP_LOGE(TAG, "LOG FAILED");
|
|
||||||
// TODO: figure out how long logging takes (for reference, and comp to wdt)
|
// POST checks — verify hardware is responding correctly
|
||||||
|
adc_post(); // ADC channels readable and not frozen
|
||||||
|
storage_post(); // flash write-read-verify on test sector
|
||||||
|
|
||||||
|
//run_all_log_tests();
|
||||||
|
|
||||||
esp_reset_reason_t reset_reason = esp_reset_reason();
|
esp_reset_reason_t reset_reason = esp_reset_reason();
|
||||||
esp_sleep_wakeup_cause_t wake_cause = esp_sleep_get_wakeup_cause();
|
esp_sleep_wakeup_cause_t wake_cause = esp_sleep_get_wakeup_cause();
|
||||||
@@ -189,48 +223,74 @@ void app_main(void) {esp_task_wdt_add(NULL);
|
|||||||
log_write(boot_entry, sizeof(boot_entry), LOG_TYPE_BOOT);
|
log_write(boot_entry, sizeof(boot_entry), LOG_TYPE_BOOT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make sure that this is "crash proof"
|
// OTA rollback: count consecutive abnormal resets (panic/WDT).
|
||||||
// TODO: OTA rollback (triggered how? preferably with hardware... or if there are 5 resets in a row [check bootloader?]. also need way to nuke the storage partition or safe boot)
|
// Power-on and external resets clear the counter; crashes increment it.
|
||||||
// TODO: (maybe) recovery partition that allows uploading firmware
|
// After OTA_ROLLBACK_THRESHOLD consecutive crashes, roll back to the
|
||||||
// Write a crash log entry if we rebooted unexpectedly
|
// previous OTA partition (if available).
|
||||||
if (reset_reason == ESP_RST_PANIC ||
|
if (reset_reason == ESP_RST_POWERON || reset_reason == ESP_RST_EXT) {
|
||||||
|
ota_reset_counter = 0;
|
||||||
|
} else if (reset_reason == ESP_RST_PANIC ||
|
||||||
reset_reason == ESP_RST_INT_WDT ||
|
reset_reason == ESP_RST_INT_WDT ||
|
||||||
reset_reason == ESP_RST_TASK_WDT ||
|
reset_reason == ESP_RST_TASK_WDT ||
|
||||||
reset_reason == ESP_RST_WDT) {
|
reset_reason == ESP_RST_WDT) {
|
||||||
ESP_LOGW(TAG, "Crash detected! Reset reason: %d", reset_reason);
|
ota_reset_counter++;
|
||||||
|
ESP_LOGW(TAG, "Crash detected (reason=%d), reset counter=%d/%d",
|
||||||
|
reset_reason, ota_reset_counter, OTA_ROLLBACK_THRESHOLD);
|
||||||
|
|
||||||
uint8_t crash_entry[9] = {};
|
uint8_t crash_entry[9] = {};
|
||||||
uint64_t ts = rtc_get_ms();
|
uint64_t ts = rtc_get_ms();
|
||||||
memcpy(&crash_entry[0], &ts, 8);
|
memcpy(&crash_entry[0], &ts, 8);
|
||||||
crash_entry[8] = (uint8_t)reset_reason;
|
crash_entry[8] = (uint8_t)reset_reason;
|
||||||
log_write(crash_entry, sizeof(crash_entry), LOG_TYPE_CRASH);
|
log_write(crash_entry, sizeof(crash_entry), LOG_TYPE_CRASH);
|
||||||
|
|
||||||
|
if (ota_reset_counter >= OTA_ROLLBACK_THRESHOLD) {
|
||||||
|
ESP_LOGE(TAG, "Rollback threshold reached — marking app invalid");
|
||||||
|
esp_ota_mark_app_invalid_rollback_and_reboot();
|
||||||
|
// Does not return — reboots into previous OTA slot
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: is this reasonable now that we eliminated deep sleep?
|
|
||||||
if (solar_run_fsm() != ESP_OK) ESP_LOGE(TAG, "SOLAR FAILED");
|
if (solar_run_fsm() != ESP_OK) ESP_LOGE(TAG, "SOLAR FAILED");
|
||||||
// TODO: Do a 12V check and enter deep sleep if there's a problem
|
|
||||||
|
|
||||||
send_bat_log();
|
send_bat_log();
|
||||||
|
|
||||||
// TODO: test strategy!!! (software verification, and unit bringup)
|
/*** FULL BOOT ***/
|
||||||
// TODO: A->D bringup; sanity check (sum up all inputs, wait 5ms, sum again, make sure there is a change (not frozen))
|
// Critical — must succeed or reboot
|
||||||
|
init_critical("UART", uart_init);
|
||||||
|
init_critical("SENSORS", sensors_init);
|
||||||
|
init_critical("FSM", fsm_init);
|
||||||
|
|
||||||
// TODO: make sure sdkconfig is sane. Make notes, have claude figure this out properly
|
// Create event group before non-critical inits (they set bits on it)
|
||||||
// TODO: fix managed_components
|
comms_event_group = xEventGroupCreate();
|
||||||
|
|
||||||
//send_log();
|
// Non-critical — retry once on failure, then log and continue.
|
||||||
|
// Set event bits even on failure so alarm-wake doesn't block forever.
|
||||||
|
if (rf_433_init() != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "RF init failed, retrying...");
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(200));
|
||||||
|
if (rf_433_init() != ESP_OK) ESP_LOGE(TAG, "RF FAILED (continuing without RF)");
|
||||||
|
}
|
||||||
|
if (bt_hid_init() != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "BT init failed, retrying...");
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(200));
|
||||||
|
if (bt_hid_init() != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "BT HID FAILED (continuing without BT)");
|
||||||
|
if (comms_event_group) xEventGroupSetBits(comms_event_group, BT_READY_BIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (webserver_init() != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Webserver init failed, retrying...");
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
if (webserver_init() != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "WEBSERVER FAILED (continuing without WiFi)");
|
||||||
|
if (comms_event_group) xEventGroupSetBits(comms_event_group, WIFI_READY_BIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//write_dummy_log_1();
|
// POST + FSM started successfully — this firmware is good.
|
||||||
|
// Clear the rollback counter and mark the OTA partition as valid.
|
||||||
/*** FULL BOOT — always, every boot ***/
|
ota_reset_counter = 0;
|
||||||
if (uart_init() != ESP_OK) ESP_LOGE(TAG, "UART FAILED");
|
esp_ota_mark_app_valid_cancel_rollback();
|
||||||
//if (power_init() != ESP_OK) ESP_LOGE(TAG, "POWER FAILED");
|
|
||||||
|
|
||||||
// TODO: Seriously, log all the errors on bluetooth
|
|
||||||
if (rf_433_init() != ESP_OK) ESP_LOGE(TAG, "RF FAILED");
|
|
||||||
if (bt_hid_init() != ESP_OK) ESP_LOGE(TAG, "BT HID FAILED");
|
|
||||||
if (fsm_init() != ESP_OK) ESP_LOGE(TAG, "FSM FAILED");
|
|
||||||
//if (sensors_init() != ESP_OK) ESP_LOGE(TAG, "SENSORS FAILED"); // TODO: Why is this off?
|
|
||||||
if (webserver_init() != ESP_OK) ESP_LOGE(TAG, "WEBSERVER FAILED");
|
|
||||||
|
|
||||||
/*** MAIN LOOP ***/
|
/*** MAIN LOOP ***/
|
||||||
TickType_t xLastWakeTime = xTaskGetTickCount();
|
TickType_t xLastWakeTime = xTaskGetTickCount();
|
||||||
@@ -252,8 +312,12 @@ void app_main(void) {esp_task_wdt_add(NULL);
|
|||||||
if (rtc_alarm_tripped()) {
|
if (rtc_alarm_tripped()) {
|
||||||
soft_idle_exit();
|
soft_idle_exit();
|
||||||
xLastWakeTime = xTaskGetTickCount();
|
xLastWakeTime = xTaskGetTickCount();
|
||||||
vTaskDelay(pdMS_TO_TICKS(500));
|
// Wait for WiFi + BT to come back up (or timeout after 5s)
|
||||||
// TODO: do a hard wait until wifi and bluetooth come up, not just blindly wait; might be better to be non-blocking
|
if (comms_event_group) {
|
||||||
|
xEventGroupWaitBits(comms_event_group, COMMS_ALL_BITS,
|
||||||
|
pdFALSE, pdTRUE, pdMS_TO_TICKS(5000));
|
||||||
|
}
|
||||||
|
esp_task_wdt_reset();
|
||||||
fsm_request(FSM_CMD_START);
|
fsm_request(FSM_CMD_START);
|
||||||
rtc_schedule_next_alarm();
|
rtc_schedule_next_alarm();
|
||||||
}
|
}
|
||||||
@@ -362,7 +426,7 @@ void app_main(void) {esp_task_wdt_add(NULL);
|
|||||||
}
|
}
|
||||||
|
|
||||||
solar_run_fsm();
|
solar_run_fsm();
|
||||||
rtc_check_shutdown_timer(); // TODO: Will esp timer overflow? Handle overflow if needed (this used to be handled by the fact that we were in deep sleep)
|
rtc_check_shutdown_timer();
|
||||||
esp_task_wdt_reset();
|
esp_task_wdt_reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
467
main/partition_test.c
Normal file
467
main/partition_test.c
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
#include "partition_test.h"
|
||||||
|
#include "storage.h"
|
||||||
|
#include "esp_partition.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_task_wdt.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define TAG "PART_TEST"
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Test 1: Params partition read/write
|
||||||
|
// ============================================================================
|
||||||
|
bool test_params_partition_rw(void) {
|
||||||
|
ESP_LOGI(TAG, "=== Test: params partition read/write ===");
|
||||||
|
|
||||||
|
const esp_partition_t *part = esp_partition_find_first(
|
||||||
|
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "params");
|
||||||
|
if (part == NULL) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: params partition not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "params partition: offset=0x%lx size=%lu",
|
||||||
|
(unsigned long)part->address, (unsigned long)part->size);
|
||||||
|
|
||||||
|
// Erase first sector
|
||||||
|
esp_err_t err = esp_partition_erase_range(part, 0, FLASH_SECTOR_SIZE);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: erase failed: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write test pattern
|
||||||
|
uint8_t write_buf[32];
|
||||||
|
for (int i = 0; i < 32; i++) write_buf[i] = (uint8_t)(0xAA ^ i);
|
||||||
|
|
||||||
|
err = esp_partition_write(part, 0, write_buf, sizeof(write_buf));
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: write failed: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read back and verify
|
||||||
|
uint8_t read_buf[32];
|
||||||
|
err = esp_partition_read(part, 0, read_buf, sizeof(read_buf));
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: read failed: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memcmp(write_buf, read_buf, sizeof(write_buf)) != 0) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: data mismatch");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify erased area reads 0xFF
|
||||||
|
uint8_t erased_check;
|
||||||
|
err = esp_partition_read(part, 64, &erased_check, 1);
|
||||||
|
if (err != ESP_OK || erased_check != 0xFF) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: erased area not 0xFF (got 0x%02X)", erased_check);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
esp_partition_erase_range(part, 0, FLASH_SECTOR_SIZE);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "PASS");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Test 2: Log partition read/write
|
||||||
|
// ============================================================================
|
||||||
|
bool test_log_partition_rw(void) {
|
||||||
|
ESP_LOGI(TAG, "=== Test: log partition read/write ===");
|
||||||
|
|
||||||
|
const esp_partition_t *part = esp_partition_find_first(
|
||||||
|
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "log");
|
||||||
|
if (part == NULL) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: log partition not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "log partition: offset=0x%lx size=%lu",
|
||||||
|
(unsigned long)part->address, (unsigned long)part->size);
|
||||||
|
|
||||||
|
// Verify size matches expectations (108K = 27 sectors)
|
||||||
|
uint32_t expected_sectors = part->size / FLASH_SECTOR_SIZE;
|
||||||
|
ESP_LOGI(TAG, "log partition has %lu sectors", (unsigned long)expected_sectors);
|
||||||
|
|
||||||
|
// Test write at start of partition
|
||||||
|
esp_err_t err = esp_partition_erase_range(part, 0, FLASH_SECTOR_SIZE);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: erase sector 0 failed: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t write_buf[16] = {0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04,
|
||||||
|
0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C};
|
||||||
|
err = esp_partition_write(part, 0, write_buf, sizeof(write_buf));
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: write failed: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t read_buf[16];
|
||||||
|
err = esp_partition_read(part, 0, read_buf, sizeof(read_buf));
|
||||||
|
if (err != ESP_OK || memcmp(write_buf, read_buf, sizeof(write_buf)) != 0) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: read-back mismatch at offset 0");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test write at last sector
|
||||||
|
uint32_t last_sector = part->size - FLASH_SECTOR_SIZE;
|
||||||
|
err = esp_partition_erase_range(part, last_sector, FLASH_SECTOR_SIZE);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: erase last sector failed: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = esp_partition_write(part, last_sector, write_buf, sizeof(write_buf));
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: write to last sector failed: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = esp_partition_read(part, last_sector, read_buf, sizeof(read_buf));
|
||||||
|
if (err != ESP_OK || memcmp(write_buf, read_buf, sizeof(write_buf)) != 0) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: read-back mismatch at last sector");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
esp_partition_erase_range(part, 0, FLASH_SECTOR_SIZE);
|
||||||
|
esp_partition_erase_range(part, last_sector, FLASH_SECTOR_SIZE);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "PASS");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Test 3: POST test partition read/write
|
||||||
|
// ============================================================================
|
||||||
|
bool test_post_partition_rw(void) {
|
||||||
|
ESP_LOGI(TAG, "=== Test: post_test partition read/write ===");
|
||||||
|
|
||||||
|
const esp_partition_t *part = esp_partition_find_first(
|
||||||
|
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "post_test");
|
||||||
|
if (part == NULL) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: post_test partition not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "post_test partition: offset=0x%lx size=%lu",
|
||||||
|
(unsigned long)part->address, (unsigned long)part->size);
|
||||||
|
|
||||||
|
// Verify it's exactly 4K (1 sector)
|
||||||
|
if (part->size != FLASH_SECTOR_SIZE) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: expected 4096 bytes, got %lu", (unsigned long)part->size);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the actual storage_post() function
|
||||||
|
esp_err_t err = storage_post();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: storage_post() returned %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the sector is clean after POST (it erases on completion)
|
||||||
|
uint8_t check;
|
||||||
|
err = esp_partition_read(part, 0, &check, 1);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: read after POST failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (check != 0xFF) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: POST didn't clean up (byte 0 = 0x%02X)", check);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "PASS");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Test 4: Partitions are independent (writing to one doesn't corrupt another)
|
||||||
|
// ============================================================================
|
||||||
|
bool test_partitions_independent(void) {
|
||||||
|
ESP_LOGI(TAG, "=== Test: partition independence ===");
|
||||||
|
|
||||||
|
const esp_partition_t *params = esp_partition_find_first(
|
||||||
|
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "params");
|
||||||
|
const esp_partition_t *log = esp_partition_find_first(
|
||||||
|
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "log");
|
||||||
|
const esp_partition_t *post = esp_partition_find_first(
|
||||||
|
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "post_test");
|
||||||
|
|
||||||
|
if (!params || !log || !post) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: one or more partitions not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no overlap
|
||||||
|
uint32_t params_end = params->address + params->size;
|
||||||
|
uint32_t log_end = log->address + log->size;
|
||||||
|
uint32_t post_end = post->address + post->size;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "params: 0x%lx - 0x%lx", (unsigned long)params->address, (unsigned long)params_end);
|
||||||
|
ESP_LOGI(TAG, "log: 0x%lx - 0x%lx", (unsigned long)log->address, (unsigned long)log_end);
|
||||||
|
ESP_LOGI(TAG, "post: 0x%lx - 0x%lx", (unsigned long)post->address, (unsigned long)post_end);
|
||||||
|
|
||||||
|
bool overlap = false;
|
||||||
|
if (params->address < log_end && log->address < params_end) overlap = true;
|
||||||
|
if (params->address < post_end && post->address < params_end) overlap = true;
|
||||||
|
if (log->address < post_end && post->address < log_end) overlap = true;
|
||||||
|
|
||||||
|
if (overlap) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: partitions overlap!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a sentinel to each partition, then verify none were corrupted
|
||||||
|
esp_partition_erase_range(params, 0, FLASH_SECTOR_SIZE);
|
||||||
|
esp_partition_erase_range(log, 0, FLASH_SECTOR_SIZE);
|
||||||
|
esp_partition_erase_range(post, 0, FLASH_SECTOR_SIZE);
|
||||||
|
|
||||||
|
uint8_t pat_params[4] = {0x11, 0x22, 0x33, 0x44};
|
||||||
|
uint8_t pat_log[4] = {0x55, 0x66, 0x77, 0x88};
|
||||||
|
uint8_t pat_post[4] = {0x99, 0xAA, 0xBB, 0xCC};
|
||||||
|
|
||||||
|
esp_partition_write(params, 0, pat_params, 4);
|
||||||
|
esp_partition_write(log, 0, pat_log, 4);
|
||||||
|
esp_partition_write(post, 0, pat_post, 4);
|
||||||
|
|
||||||
|
// Read back all three and verify
|
||||||
|
uint8_t rb[4];
|
||||||
|
|
||||||
|
esp_partition_read(params, 0, rb, 4);
|
||||||
|
if (memcmp(rb, pat_params, 4) != 0) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: params sentinel corrupted");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_partition_read(log, 0, rb, 4);
|
||||||
|
if (memcmp(rb, pat_log, 4) != 0) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: log sentinel corrupted");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_partition_read(post, 0, rb, 4);
|
||||||
|
if (memcmp(rb, pat_post, 4) != 0) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: post sentinel corrupted");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
esp_partition_erase_range(params, 0, FLASH_SECTOR_SIZE);
|
||||||
|
esp_partition_erase_range(log, 0, FLASH_SECTOR_SIZE);
|
||||||
|
esp_partition_erase_range(post, 0, FLASH_SECTOR_SIZE);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "PASS");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Test 5: Parameter commit and reload
|
||||||
|
// ============================================================================
|
||||||
|
bool test_params_persist_after_commit(void) {
|
||||||
|
ESP_LOGI(TAG, "=== Test: params persist after commit ===");
|
||||||
|
|
||||||
|
// Save original value
|
||||||
|
param_value_t original = get_param_value_t(PARAM_DRIVE_DIST);
|
||||||
|
float orig_val = original.f32;
|
||||||
|
|
||||||
|
// Set a distinctive test value
|
||||||
|
float test_val = 99.99f;
|
||||||
|
param_value_t test = {.f32 = test_val};
|
||||||
|
esp_err_t err = set_param_value_t(PARAM_DRIVE_DIST, test);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: set_param_value_t failed: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit to flash
|
||||||
|
err = commit_params();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: commit_params failed: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the value stuck in RAM
|
||||||
|
param_value_t readback = get_param_value_t(PARAM_DRIVE_DIST);
|
||||||
|
if (readback.f32 != test_val) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: RAM value mismatch (got %.2f, expected %.2f)",
|
||||||
|
readback.f32, test_val);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-init storage to force reload from flash
|
||||||
|
err = storage_init();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: storage_init failed on reload: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
readback = get_param_value_t(PARAM_DRIVE_DIST);
|
||||||
|
if (readback.f32 != test_val) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: flash value mismatch after reload (got %.2f, expected %.2f)",
|
||||||
|
readback.f32, test_val);
|
||||||
|
// Restore original before returning
|
||||||
|
param_value_t orig = {.f32 = orig_val};
|
||||||
|
set_param_value_t(PARAM_DRIVE_DIST, orig);
|
||||||
|
commit_params();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore original value
|
||||||
|
param_value_t orig = {.f32 = orig_val};
|
||||||
|
set_param_value_t(PARAM_DRIVE_DIST, orig);
|
||||||
|
err = commit_params();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "WARN: failed to restore original value");
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "PASS");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Test 6: Log write/read cycle through the log API
|
||||||
|
// ============================================================================
|
||||||
|
bool test_log_write_read_cycle(void) {
|
||||||
|
ESP_LOGI(TAG, "=== Test: log write/read cycle ===");
|
||||||
|
|
||||||
|
// Erase and reinit log
|
||||||
|
esp_err_t err = log_erase_all_sectors();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: log_erase_all_sectors failed: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
|
||||||
|
// Write 3 entries with different types
|
||||||
|
uint8_t data1[] = {0x01, 0x02, 0x03, 0x04};
|
||||||
|
uint8_t data2[] = {0xAA, 0xBB, 0xCC};
|
||||||
|
uint8_t data3[] = {0xFF, 0xFE, 0xFD, 0xFC, 0xFB};
|
||||||
|
|
||||||
|
err = log_write_blocking_test(data1, sizeof(data1), LOG_TYPE_DATA);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: write 1 failed: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = log_write_blocking_test(data2, sizeof(data2), LOG_TYPE_EVENT);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: write 2 failed: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = log_write_blocking_test(data3, sizeof(data3), LOG_TYPE_SENSOR);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: write 3 failed: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for writes to flush through the queue
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
|
||||||
|
// Read them back
|
||||||
|
log_read_reset();
|
||||||
|
uint8_t read_buf[LOG_MAX_PAYLOAD];
|
||||||
|
uint8_t read_len, read_type;
|
||||||
|
|
||||||
|
// Entry 1
|
||||||
|
err = log_read(&read_len, read_buf, &read_type);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: read 1 failed: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (read_len != sizeof(data1) || read_type != LOG_TYPE_DATA ||
|
||||||
|
memcmp(read_buf, data1, sizeof(data1)) != 0) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: entry 1 mismatch (len=%d type=0x%02X)", read_len, read_type);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry 2
|
||||||
|
err = log_read(&read_len, read_buf, &read_type);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: read 2 failed: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (read_len != sizeof(data2) || read_type != LOG_TYPE_EVENT ||
|
||||||
|
memcmp(read_buf, data2, sizeof(data2)) != 0) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: entry 2 mismatch (len=%d type=0x%02X)", read_len, read_type);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry 3
|
||||||
|
err = log_read(&read_len, read_buf, &read_type);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: read 3 failed: %s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (read_len != sizeof(data3) || read_type != LOG_TYPE_SENSOR ||
|
||||||
|
memcmp(read_buf, data3, sizeof(data3)) != 0) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: entry 3 mismatch (len=%d type=0x%02X)", read_len, read_type);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no more entries
|
||||||
|
err = log_read(&read_len, read_buf, &read_type);
|
||||||
|
if (err != ESP_ERR_NOT_FOUND) {
|
||||||
|
ESP_LOGE(TAG, "FAIL: expected no more entries, got err=%s", esp_err_to_name(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "PASS");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Test runner
|
||||||
|
// ============================================================================
|
||||||
|
esp_err_t run_partition_tests(void) {
|
||||||
|
ESP_LOGI(TAG, "");
|
||||||
|
ESP_LOGI(TAG, "=================================================");
|
||||||
|
ESP_LOGI(TAG, " PARTITION VERIFICATION TEST SUITE");
|
||||||
|
ESP_LOGI(TAG, "=================================================");
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *name;
|
||||||
|
bool (*fn)(void);
|
||||||
|
} test_entry_t;
|
||||||
|
|
||||||
|
test_entry_t tests[] = {
|
||||||
|
{"params partition r/w", test_params_partition_rw},
|
||||||
|
{"log partition r/w", test_log_partition_rw},
|
||||||
|
{"post_test partition r/w", test_post_partition_rw},
|
||||||
|
{"partition independence", test_partitions_independent},
|
||||||
|
{"params persist after commit", test_params_persist_after_commit},
|
||||||
|
{"log write/read cycle", test_log_write_read_cycle},
|
||||||
|
};
|
||||||
|
|
||||||
|
int num_tests = sizeof(tests) / sizeof(tests[0]);
|
||||||
|
int passed = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < num_tests; i++) {
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
ESP_LOGI(TAG, "");
|
||||||
|
bool result = tests[i].fn();
|
||||||
|
if (result) passed++;
|
||||||
|
else ESP_LOGE(TAG, "FAILED: %s", tests[i].name);
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "");
|
||||||
|
ESP_LOGI(TAG, "=================================================");
|
||||||
|
ESP_LOGI(TAG, " RESULTS: %d/%d passed", passed, num_tests);
|
||||||
|
ESP_LOGI(TAG, "=================================================");
|
||||||
|
|
||||||
|
return (passed == num_tests) ? ESP_OK : ESP_FAIL;
|
||||||
|
}
|
||||||
19
main/partition_test.h
Normal file
19
main/partition_test.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#ifndef PARTITION_TEST_H
|
||||||
|
#define PARTITION_TEST_H
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
// Run all partition verification tests
|
||||||
|
// Call from app_main() after storage_init() and log_init()
|
||||||
|
esp_err_t run_partition_tests(void);
|
||||||
|
|
||||||
|
// Individual tests
|
||||||
|
bool test_params_partition_rw(void);
|
||||||
|
bool test_log_partition_rw(void);
|
||||||
|
bool test_post_partition_rw(void);
|
||||||
|
bool test_partitions_independent(void);
|
||||||
|
bool test_params_persist_after_commit(void);
|
||||||
|
bool test_log_write_read_cycle(void);
|
||||||
|
|
||||||
|
#endif // PARTITION_TEST_H
|
||||||
@@ -179,6 +179,41 @@ esp_err_t adc_init() {
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
esp_err_t adc_post(void) {
|
||||||
|
// Read all 4 channels twice with a short delay; flag if frozen or wildly out of range
|
||||||
|
const adc_channel_t channels[] = { PIN_V_ISENS1, PIN_V_ISENS2, PIN_V_ISENS3, PIN_V_SENS_BAT };
|
||||||
|
const char *names[] = { "ISENS1", "ISENS2", "ISENS3", "BATTERY" };
|
||||||
|
int first[4], second[4];
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
if (adc_oneshot_read(adc1_handle, channels[i], &first[i]) != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "POST: ADC read failed on %s", names[i]);
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(5));
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
if (adc_oneshot_read(adc1_handle, channels[i], &second[i]) != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "POST: ADC read failed on %s (2nd)", names[i]);
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for frozen ADC (identical readings on noise-bearing current sense channels)
|
||||||
|
for (int i = 0; i < 3; i++) { // only current sense, not battery (battery can be stable)
|
||||||
|
if (first[i] == second[i] && first[i] != 0) {
|
||||||
|
ESP_LOGW(TAG, "POST: ADC %s may be frozen (both reads = %d)", names[i], first[i]);
|
||||||
|
// Warning only — a truly stuck ADC will trip efuse protections anyway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "POST: ADC OK (BAT=%d/%d, I1=%d/%d, I2=%d/%d, I3=%d/%d)",
|
||||||
|
first[3], second[3], first[0], second[0], first[1], second[1], first[2], second[2]);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
float get_raw_battery_voltage(void) {
|
float get_raw_battery_voltage(void) {
|
||||||
int adc_raw = 0;
|
int adc_raw = 0;
|
||||||
int voltage_mv = 0;
|
int voltage_mv = 0;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ esp_err_t process_bridge_current(bridge_t bridge);
|
|||||||
esp_err_t process_battery_voltage();
|
esp_err_t process_battery_voltage();
|
||||||
|
|
||||||
esp_err_t adc_init();
|
esp_err_t adc_init();
|
||||||
|
esp_err_t adc_post(void);
|
||||||
esp_err_t power_init();
|
esp_err_t power_init();
|
||||||
esp_err_t power_stop();
|
esp_err_t power_stop();
|
||||||
|
|
||||||
|
|||||||
@@ -220,10 +220,6 @@ esp_err_t rf_433_init() {
|
|||||||
|
|
||||||
esp_err_t rf_433_stop() { return ESP_OK; }
|
esp_err_t rf_433_stop() { return ESP_OK; }
|
||||||
|
|
||||||
void rf_433_set_keycode(uint8_t index, uint32_t code) {
|
|
||||||
set_param_value_t(PARAM_KEYCODE_0+index, (param_value_t){.u32=code});
|
|
||||||
}
|
|
||||||
|
|
||||||
void rf_433_learn_keycode(uint8_t index) {
|
void rf_433_learn_keycode(uint8_t index) {
|
||||||
if (index >= 8) return;
|
if (index >= 8) return;
|
||||||
learn_flag = index;
|
learn_flag = index;
|
||||||
|
|||||||
@@ -16,13 +16,6 @@ int64_t receive_keycode(void);
|
|||||||
esp_err_t rf_433_init();
|
esp_err_t rf_433_init();
|
||||||
esp_err_t rf_433_stop();
|
esp_err_t rf_433_stop();
|
||||||
|
|
||||||
void rf_433_set_keycode(uint8_t index, uint32_t code);
|
|
||||||
|
|
||||||
/*
|
|
||||||
int8_t rf_433_get_keycode();
|
|
||||||
int64_t rf_433_get_raw_keycode();
|
|
||||||
*/
|
|
||||||
|
|
||||||
void rf_433_learn_keycode(uint8_t index);
|
void rf_433_learn_keycode(uint8_t index);
|
||||||
void rf_433_cancel_learn_keycode();
|
void rf_433_cancel_learn_keycode();
|
||||||
|
|
||||||
|
|||||||
18
main/rtc.c
18
main/rtc.c
@@ -44,7 +44,9 @@ static uint64_t rtc_hw_time_us(void)
|
|||||||
|
|
||||||
uint64_t last_activity_tick = 0;
|
uint64_t last_activity_tick = 0;
|
||||||
|
|
||||||
// RTC_DATA_ATTR keeps these in RTC memory; persists across software resets (panics, WDT)
|
// RTC_DATA_ATTR keeps these in RTC memory; persists across software resets (panics, WDT).
|
||||||
|
// AUDIT: no init path zeroes these — rtc_restore_time() recovers via RTC HW counter,
|
||||||
|
// rtc_set_s() is only called explicitly by the user. Verified 2026-03-12.
|
||||||
RTC_DATA_ATTR int64_t next_alarm_time_s = -1;
|
RTC_DATA_ATTR int64_t next_alarm_time_s = -1;
|
||||||
RTC_DATA_ATTR bool rtc_set = false;
|
RTC_DATA_ATTR bool rtc_set = false;
|
||||||
RTC_DATA_ATTR int64_t sync_unix_us = 0; // Unix time in µs at last rtc_set_s() call
|
RTC_DATA_ATTR int64_t sync_unix_us = 0; // Unix time in µs at last rtc_set_s() call
|
||||||
@@ -160,22 +162,14 @@ int64_t rtc_get_s_in_day(void)
|
|||||||
return rtc_get_s() % 86400UL;
|
return rtc_get_s() % 86400UL;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_sleep_wakeup_cause_t rtc_wakeup_cause(void)
|
|
||||||
{
|
|
||||||
esp_sleep_wakeup_cause_t c = esp_sleep_get_wakeup_cause();
|
|
||||||
switch (c) {
|
|
||||||
case ESP_SLEEP_WAKEUP_EXT0: ESP_LOGI("RTC", "Wakeup: GPIO"); break;
|
|
||||||
case ESP_SLEEP_WAKEUP_TIMER: ESP_LOGI("RTC", "Wakeup: timer"); break;
|
|
||||||
default: ESP_LOGI("RTC", "Wakeup: normal boot"); break;
|
|
||||||
}
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Unified periodic update */
|
/* Unified periodic update */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
void rtc_check_shutdown_timer(void)
|
void rtc_check_shutdown_timer(void)
|
||||||
{
|
{
|
||||||
|
// Unsigned subtraction handles TickType_t (uint32_t) wraparound correctly:
|
||||||
|
// e.g. if tick wrapped from 0xFFFFFFFE to 5, elapsed = 5 - 0xFFFFFFFE = 7.
|
||||||
|
// At 1ms/tick, uint32_t wraps after ~49.7 days — well beyond the 180s timeout.
|
||||||
TickType_t elapsed = xTaskGetTickCount() - last_activity_tick;
|
TickType_t elapsed = xTaskGetTickCount() - last_activity_tick;
|
||||||
if (elapsed * portTICK_PERIOD_MS >= POWER_INACTIVITY_TIMEOUT_MS)
|
if (elapsed * portTICK_PERIOD_MS >= POWER_INACTIVITY_TIMEOUT_MS)
|
||||||
soft_idle_enter();
|
soft_idle_enter();
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ void soft_idle_enter(void);
|
|||||||
void soft_idle_exit(void);
|
void soft_idle_exit(void);
|
||||||
bool soft_idle_is_active(void);
|
bool soft_idle_is_active(void);
|
||||||
bool soft_idle_button_raw(void); /* direct GPIO read, no I2C */
|
bool soft_idle_button_raw(void); /* direct GPIO read, no I2C */
|
||||||
esp_sleep_wakeup_cause_t rtc_wakeup_cause();
|
|
||||||
|
|
||||||
/*void adjust_rtc_hour(char *key, int8_t dir);
|
/*void adjust_rtc_hour(char *key, int8_t dir);
|
||||||
void adjust_rtc_min(char *key, int8_t dir);*/
|
void adjust_rtc_min(char *key, int8_t dir);*/
|
||||||
|
|||||||
@@ -173,45 +173,6 @@ int8_t pack_sensors() {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*esp_err_t sensors_init() {
|
|
||||||
gpio_config_t io_conf = {
|
|
||||||
.pin_bit_mask = (1ULL << sensor_pins[0]) | (1ULL << sensor_pins[1]),
|
|
||||||
.mode = GPIO_MODE_INPUT,
|
|
||||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
|
||||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
|
||||||
.intr_type = GPIO_INTR_ANYEDGE,
|
|
||||||
};
|
|
||||||
ESP_ERROR_CHECK(gpio_config(&io_conf));
|
|
||||||
|
|
||||||
sensor_event_queue = xQueueCreate(16, sizeof(sensor_event_t));
|
|
||||||
if (!sensor_event_queue) {
|
|
||||||
ESP_LOGE(TAG, "Failed to create sensor queue");
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install ISR service
|
|
||||||
ESP_ERROR_CHECK(gpio_install_isr_service(0));
|
|
||||||
|
|
||||||
for (uint8_t i = 0; i < N_SENSORS; i++) {
|
|
||||||
ESP_ERROR_CHECK(gpio_isr_handler_add(sensor_pins[i], sensor_isr_handler, INT2VOIDP(sensor_pins[i])));
|
|
||||||
sensor_stable_state[i] = !gpio_get_level(sensor_pins[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
xTaskCreate(sensor_debounce_task, "SENSORS", 3072, NULL, 6, NULL);
|
|
||||||
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t sensors_stop() {
|
|
||||||
for (uint8_t i = 0; i < N_SENSORS; i++) {
|
|
||||||
gpio_isr_handler_remove(sensor_pins[i]);
|
|
||||||
}
|
|
||||||
gpio_uninstall_isr_service();
|
|
||||||
vQueueDelete(sensor_event_queue);
|
|
||||||
|
|
||||||
return ESP_OK;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// Public API
|
// Public API
|
||||||
bool get_sensor(sensor_t i) {
|
bool get_sensor(sensor_t i) {
|
||||||
return sensor_stable_state[i];
|
return sensor_stable_state[i];
|
||||||
|
|||||||
438
main/storage.c
438
main/storage.c
@@ -7,6 +7,7 @@
|
|||||||
#include "esp_crc.h"
|
#include "esp_crc.h"
|
||||||
#include "esp_task_wdt.h"
|
#include "esp_task_wdt.h"
|
||||||
#include "storage.h"
|
#include "storage.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/semphr.h"
|
#include "freertos/semphr.h"
|
||||||
#include "nvs_flash.h"
|
#include "nvs_flash.h"
|
||||||
@@ -60,40 +61,54 @@ typedef struct {
|
|||||||
#define PARAM_NAME_STR(name) #name
|
#define PARAM_NAME_STR(name) #name
|
||||||
|
|
||||||
// Generate parameter table with live values (initialized to defaults)
|
// Generate parameter table with live values (initialized to defaults)
|
||||||
#define PARAM_DEF(name, type, default_val, unit) PARAM_VALUE_INIT(type, default_val),
|
#define PARAM_DEF(name, type, default_val, unit, min, max) PARAM_VALUE_INIT(type, default_val),
|
||||||
param_value_t parameter_table[NUM_PARAMS] = {
|
param_value_t parameter_table[NUM_PARAMS] = {
|
||||||
PARAM_LIST
|
PARAM_LIST
|
||||||
};
|
};
|
||||||
#undef PARAM_DEF
|
#undef PARAM_DEF
|
||||||
|
|
||||||
// Generate default values array
|
// Generate default values array
|
||||||
#define PARAM_DEF(name, type, default_val, unit) PARAM_VALUE_INIT(type, default_val),
|
#define PARAM_DEF(name, type, default_val, unit, min, max) PARAM_VALUE_INIT(type, default_val),
|
||||||
const param_value_t parameter_defaults[NUM_PARAMS] = {
|
const param_value_t parameter_defaults[NUM_PARAMS] = {
|
||||||
PARAM_LIST
|
PARAM_LIST
|
||||||
};
|
};
|
||||||
#undef PARAM_DEF
|
#undef PARAM_DEF
|
||||||
|
|
||||||
// Generate parameter types array
|
// Generate parameter types array
|
||||||
#define PARAM_DEF(name, type, default_val, unit) PARAM_TYPE_ENUM(type),
|
#define PARAM_DEF(name, type, default_val, unit, min, max) PARAM_TYPE_ENUM(type),
|
||||||
const param_type_e parameter_types[NUM_PARAMS] = {
|
const param_type_e parameter_types[NUM_PARAMS] = {
|
||||||
PARAM_LIST
|
PARAM_LIST
|
||||||
};
|
};
|
||||||
#undef PARAM_DEF
|
#undef PARAM_DEF
|
||||||
|
|
||||||
// Generate parameter names array
|
// Generate parameter names array
|
||||||
#define PARAM_DEF(name, type, default_val, unit) PARAM_NAME_STR(name),
|
#define PARAM_DEF(name, type, default_val, unit, min, max) PARAM_NAME_STR(name),
|
||||||
const char* parameter_names[NUM_PARAMS] = {
|
const char* parameter_names[NUM_PARAMS] = {
|
||||||
PARAM_LIST
|
PARAM_LIST
|
||||||
};
|
};
|
||||||
#undef PARAM_DEF
|
#undef PARAM_DEF
|
||||||
|
|
||||||
// Generate parameter units array (8 chars max per unit)
|
// Generate parameter units array (8 chars max per unit)
|
||||||
#define PARAM_DEF(name, type, default_val, unit) unit,
|
#define PARAM_DEF(name, type, default_val, unit, min, max) unit,
|
||||||
const char parameter_units[NUM_PARAMS][8] = {
|
const char parameter_units[NUM_PARAMS][8] = {
|
||||||
PARAM_LIST
|
PARAM_LIST
|
||||||
};
|
};
|
||||||
#undef PARAM_DEF
|
#undef PARAM_DEF
|
||||||
|
|
||||||
|
// Generate parameter min bounds array
|
||||||
|
#define PARAM_DEF(name, type, default_val, unit, min, max) PARAM_VALUE_INIT(type, min),
|
||||||
|
static const param_value_t parameter_mins[NUM_PARAMS] = {
|
||||||
|
PARAM_LIST
|
||||||
|
};
|
||||||
|
#undef PARAM_DEF
|
||||||
|
|
||||||
|
// Generate parameter max bounds array
|
||||||
|
#define PARAM_DEF(name, type, default_val, unit, min, max) PARAM_VALUE_INIT(type, max),
|
||||||
|
static const param_value_t parameter_maxs[NUM_PARAMS] = {
|
||||||
|
PARAM_LIST
|
||||||
|
};
|
||||||
|
#undef PARAM_DEF
|
||||||
|
|
||||||
size_t param_type_size(param_type_e x) {
|
size_t param_type_size(param_type_e x) {
|
||||||
switch(x) {
|
switch(x) {
|
||||||
case PARAM_TYPE_u16: return 2;
|
case PARAM_TYPE_u16: return 2;
|
||||||
@@ -107,11 +122,105 @@ size_t param_type_size(param_type_e x) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Partition pointer
|
// ============================================================================
|
||||||
static const esp_partition_t *storage_partition = NULL;
|
// PARAMETER VALIDATION
|
||||||
|
// ============================================================================
|
||||||
|
// Returns true if the value was modified (clamped or reset to default).
|
||||||
|
// - Strings: always skipped
|
||||||
|
// - Float NaN/Inf: reset to default
|
||||||
|
// - min == max (same raw bytes): skip bounds check (sentinel for "no bounds")
|
||||||
|
// - Otherwise: clamp to [min, max]
|
||||||
|
static bool validate_param(param_idx_t id) {
|
||||||
|
param_type_e type = parameter_types[id];
|
||||||
|
|
||||||
// Log head/tail tracking with mutex protection
|
// String params: no numeric validation
|
||||||
// These now track byte offsets within the log area, not entry indices
|
if (type == PARAM_TYPE_str) return false;
|
||||||
|
|
||||||
|
// Float types: NaN/Inf → reset to default
|
||||||
|
if (type == PARAM_TYPE_f32) {
|
||||||
|
if (isnanf(parameter_table[id].f32) || isinff(parameter_table[id].f32)) {
|
||||||
|
ESP_LOGW(TAG, "Param %s: NaN/Inf, reset to default", parameter_names[id]);
|
||||||
|
parameter_table[id] = parameter_defaults[id];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type == PARAM_TYPE_f64) {
|
||||||
|
if (isnan(parameter_table[id].f64) || isinf(parameter_table[id].f64)) {
|
||||||
|
ESP_LOGW(TAG, "Param %s: NaN/Inf, reset to default", parameter_names[id]);
|
||||||
|
parameter_table[id] = parameter_defaults[id];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip bounds check if min == max (sentinel)
|
||||||
|
size_t sz = param_type_size(type);
|
||||||
|
if (memcmp(¶meter_mins[id], ¶meter_maxs[id], sz) == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Clamp to [min, max] per type
|
||||||
|
bool clamped = false;
|
||||||
|
switch (type) {
|
||||||
|
case PARAM_TYPE_u16:
|
||||||
|
if (parameter_table[id].u16 < parameter_mins[id].u16) {
|
||||||
|
parameter_table[id].u16 = parameter_mins[id].u16; clamped = true;
|
||||||
|
} else if (parameter_table[id].u16 > parameter_maxs[id].u16) {
|
||||||
|
parameter_table[id].u16 = parameter_maxs[id].u16; clamped = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PARAM_TYPE_i16:
|
||||||
|
if (parameter_table[id].i16 < parameter_mins[id].i16) {
|
||||||
|
parameter_table[id].i16 = parameter_mins[id].i16; clamped = true;
|
||||||
|
} else if (parameter_table[id].i16 > parameter_maxs[id].i16) {
|
||||||
|
parameter_table[id].i16 = parameter_maxs[id].i16; clamped = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PARAM_TYPE_u32:
|
||||||
|
if (parameter_table[id].u32 < parameter_mins[id].u32) {
|
||||||
|
parameter_table[id].u32 = parameter_mins[id].u32; clamped = true;
|
||||||
|
} else if (parameter_table[id].u32 > parameter_maxs[id].u32) {
|
||||||
|
parameter_table[id].u32 = parameter_maxs[id].u32; clamped = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PARAM_TYPE_i32:
|
||||||
|
if (parameter_table[id].i32 < parameter_mins[id].i32) {
|
||||||
|
parameter_table[id].i32 = parameter_mins[id].i32; clamped = true;
|
||||||
|
} else if (parameter_table[id].i32 > parameter_maxs[id].i32) {
|
||||||
|
parameter_table[id].i32 = parameter_maxs[id].i32; clamped = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PARAM_TYPE_f32:
|
||||||
|
if (parameter_table[id].f32 < parameter_mins[id].f32) {
|
||||||
|
parameter_table[id].f32 = parameter_mins[id].f32; clamped = true;
|
||||||
|
} else if (parameter_table[id].f32 > parameter_maxs[id].f32) {
|
||||||
|
parameter_table[id].f32 = parameter_maxs[id].f32; clamped = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PARAM_TYPE_f64:
|
||||||
|
if (parameter_table[id].f64 < parameter_mins[id].f64) {
|
||||||
|
parameter_table[id].f64 = parameter_mins[id].f64; clamped = true;
|
||||||
|
} else if (parameter_table[id].f64 > parameter_maxs[id].f64) {
|
||||||
|
parameter_table[id].f64 = parameter_maxs[id].f64; clamped = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clamped) {
|
||||||
|
ESP_LOGW(TAG, "Param %s: out of range, clamped", parameter_names[id]);
|
||||||
|
}
|
||||||
|
return clamped;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Partition pointers (separate partitions for params, log, and POST test)
|
||||||
|
static const esp_partition_t *params_partition = NULL;
|
||||||
|
static const esp_partition_t *log_partition = NULL;
|
||||||
|
static const esp_partition_t *post_partition = NULL;
|
||||||
|
|
||||||
|
// Log head/tail tracking with mutex protection.
|
||||||
|
// These track byte offsets within the log partition (0-based).
|
||||||
|
// RTC_DATA_ATTR is historical — log_init() always recovers these from a flash scan,
|
||||||
|
// so the RTC values are overwritten on every boot. No partial-write risk.
|
||||||
RTC_DATA_ATTR static uint32_t log_head_offset = 0;
|
RTC_DATA_ATTR static uint32_t log_head_offset = 0;
|
||||||
RTC_DATA_ATTR static uint32_t log_tail_offset = 0;
|
RTC_DATA_ATTR static uint32_t log_tail_offset = 0;
|
||||||
RTC_DATA_ATTR static bool log_initialized = false;
|
RTC_DATA_ATTR static bool log_initialized = false;
|
||||||
@@ -135,7 +244,12 @@ uint32_t log_get_tail(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t log_get_offset(void) {
|
uint32_t log_get_offset(void) {
|
||||||
return LOG_START_OFFSET;
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t log_get_size(void) {
|
||||||
|
if (log_partition == NULL) return 0;
|
||||||
|
return log_partition->size;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -350,27 +464,29 @@ static void unpack_param(const uint8_t *src, param_idx_t id) {
|
|||||||
// COMMIT PARAMETERS TO FLASH
|
// COMMIT PARAMETERS TO FLASH
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
esp_err_t commit_params(void) {
|
esp_err_t commit_params(void) {
|
||||||
if (storage_partition == NULL) {
|
if (params_partition == NULL) {
|
||||||
ESP_LOGE(TAG, "Storage partition not initialized");
|
ESP_LOGE(TAG, "Params partition not initialized");
|
||||||
return ESP_ERR_INVALID_STATE;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Committing %d parameters to flash...", NUM_PARAMS);
|
ESP_LOGI(TAG, "Committing %d parameters to flash...", NUM_PARAMS);
|
||||||
|
|
||||||
// Erase parameter sectors first
|
// Erase entire params partition
|
||||||
esp_err_t err = esp_partition_erase_range(storage_partition, PARAMS_OFFSET,
|
esp_err_t err = esp_partition_erase_range(params_partition, 0, params_partition->size);
|
||||||
PARAMETER_NUM_SECTORS * FLASH_SECTOR_SIZE);
|
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to erase parameter sectors: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "Failed to erase params partition: %s", esp_err_to_name(err));
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write each parameter with CRC
|
// Write each parameter with CRC
|
||||||
uint32_t flash_offset = PARAMS_OFFSET;
|
uint32_t flash_offset = 0;
|
||||||
for (int i = 0; i < NUM_PARAMS; i++) {
|
for (int i = 0; i < NUM_PARAMS; i++) {
|
||||||
param_stored_t stored;
|
param_stored_t stored;
|
||||||
memset(&stored, 0, sizeof(param_stored_t));
|
memset(&stored, 0, sizeof(param_stored_t));
|
||||||
|
|
||||||
|
// Validate before writing — clamp out-of-range, reset NaN/Inf
|
||||||
|
validate_param(i);
|
||||||
|
|
||||||
// Pack parameter data
|
// Pack parameter data
|
||||||
pack_param(stored.data, i);
|
pack_param(stored.data, i);
|
||||||
|
|
||||||
@@ -380,7 +496,7 @@ esp_err_t commit_params(void) {
|
|||||||
stored.crc = esp_crc32_le(crc_input, stored.data, size);
|
stored.crc = esp_crc32_le(crc_input, stored.data, size);
|
||||||
|
|
||||||
// Write to flash
|
// Write to flash
|
||||||
err = esp_partition_write(storage_partition, flash_offset,
|
err = esp_partition_write(params_partition, flash_offset,
|
||||||
&stored, sizeof(param_stored_t));
|
&stored, sizeof(param_stored_t));
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to write parameter %d (%s): %s",
|
ESP_LOGE(TAG, "Failed to write parameter %d (%s): %s",
|
||||||
@@ -406,19 +522,96 @@ esp_err_t factory_reset(void) {
|
|||||||
memcpy(¶meter_table[i], ¶meter_defaults[i], sizeof(param_value_t));
|
memcpy(¶meter_table[i], ¶meter_defaults[i], sizeof(param_value_t));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: WIPE ENTIRE PARTITION
|
// Commit defaults to params partition
|
||||||
|
|
||||||
// Commit defaults to flash
|
|
||||||
esp_err_t err = commit_params();
|
esp_err_t err = commit_params();
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to commit defaults during factory reset");
|
ESP_LOGE(TAG, "Failed to commit defaults during factory reset");
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Erase the log partition
|
||||||
|
const esp_partition_t *log_part = esp_partition_find_first(
|
||||||
|
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "log");
|
||||||
|
if (log_part != NULL) {
|
||||||
|
ESP_LOGI(TAG, "Erasing log partition (%lu bytes)...", (unsigned long)log_part->size);
|
||||||
|
err = esp_partition_erase_range(log_part, 0, log_part->size);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Failed to erase log partition: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erase the POST test partition
|
||||||
|
const esp_partition_t *post_part = esp_partition_find_first(
|
||||||
|
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "post_test");
|
||||||
|
if (post_part != NULL) {
|
||||||
|
err = esp_partition_erase_range(post_part, 0, post_part->size);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Failed to erase post_test partition: %s", esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset log state so next boot starts fresh
|
||||||
|
log_head_offset = 0;
|
||||||
|
log_tail_offset = 0;
|
||||||
|
log_initialized = false;
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Factory reset complete");
|
ESP_LOGI(TAG, "Factory reset complete");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// FLASH POST (Power-On Self-Test)
|
||||||
|
// ============================================================================
|
||||||
|
esp_err_t storage_post(void) {
|
||||||
|
if (post_partition == NULL) {
|
||||||
|
// Find post_test partition if not already found
|
||||||
|
post_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
|
||||||
|
ESP_PARTITION_SUBTYPE_ANY,
|
||||||
|
"post_test");
|
||||||
|
if (post_partition == NULL) {
|
||||||
|
ESP_LOGE(TAG, "POST: post_test partition not found");
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t write_buf[16];
|
||||||
|
uint8_t read_buf[16];
|
||||||
|
|
||||||
|
// Fill with a pattern based on boot time so we don't pass on stale data
|
||||||
|
uint32_t seed = (uint32_t)esp_timer_get_time();
|
||||||
|
for (int i = 0; i < 16; i++) write_buf[i] = (uint8_t)(seed + i * 37);
|
||||||
|
|
||||||
|
esp_err_t err = esp_partition_erase_range(post_partition, 0, FLASH_SECTOR_SIZE);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "POST: flash erase failed: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = esp_partition_write(post_partition, 0, write_buf, sizeof(write_buf));
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "POST: flash write failed: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = esp_partition_read(post_partition, 0, read_buf, sizeof(read_buf));
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "POST: flash read failed: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memcmp(write_buf, read_buf, sizeof(write_buf)) != 0) {
|
||||||
|
ESP_LOGE(TAG, "POST: flash verify MISMATCH");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erase the test sector so it's clean for next boot
|
||||||
|
esp_partition_erase_range(post_partition, 0, FLASH_SECTOR_SIZE);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "POST: flash OK");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// STORAGE INITIALIZATION
|
// STORAGE INITIALIZATION
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -444,26 +637,26 @@ esp_err_t storage_init(void) {
|
|||||||
}
|
}
|
||||||
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
||||||
|
|
||||||
storage_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
|
params_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
|
||||||
ESP_PARTITION_SUBTYPE_ANY,
|
ESP_PARTITION_SUBTYPE_ANY,
|
||||||
"storage");
|
"params");
|
||||||
if (storage_partition == NULL) {
|
if (params_partition == NULL) {
|
||||||
ESP_LOGE(TAG, "Storage partition not found");
|
ESP_LOGE(TAG, "Params partition not found");
|
||||||
if (log_mutex) xSemaphoreGive(log_mutex);
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
||||||
return ESP_ERR_NOT_FOUND;
|
return ESP_ERR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Storage partition found: size=%lu bytes",
|
ESP_LOGI(TAG, "Params partition found: size=%lu bytes",
|
||||||
(unsigned long)storage_partition->size);
|
(unsigned long)params_partition->size);
|
||||||
|
|
||||||
// Load parameters from flash
|
// Load parameters from flash
|
||||||
uint32_t flash_offset = PARAMS_OFFSET;
|
uint32_t flash_offset = 0;
|
||||||
//bool all_valid = true;
|
//bool all_valid = true;
|
||||||
|
|
||||||
for (int i = 0; i < NUM_PARAMS; i++) {
|
for (int i = 0; i < NUM_PARAMS; i++) {
|
||||||
param_stored_t stored;
|
param_stored_t stored;
|
||||||
|
|
||||||
esp_err_t err = esp_partition_read(storage_partition, flash_offset,
|
esp_err_t err = esp_partition_read(params_partition, flash_offset,
|
||||||
&stored, sizeof(param_stored_t));
|
&stored, sizeof(param_stored_t));
|
||||||
|
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
@@ -482,6 +675,9 @@ esp_err_t storage_init(void) {
|
|||||||
|
|
||||||
if (calculated_crc == stored.crc) {
|
if (calculated_crc == stored.crc) {
|
||||||
unpack_param(stored.data, i);
|
unpack_param(stored.data, i);
|
||||||
|
if (validate_param(i)) {
|
||||||
|
ESP_LOGW(TAG, "Param %d (%s) out of range after load, clamped", i, parameter_names[i]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGW(TAG, "Parameter %d (%s) failed CRC check, using default",
|
ESP_LOGW(TAG, "Parameter %d (%s) failed CRC check, using default",
|
||||||
i, parameter_names[i]);
|
i, parameter_names[i]);
|
||||||
@@ -509,49 +705,27 @@ static inline uint32_t log_sector_end(uint32_t x) {
|
|||||||
|
|
||||||
// Helper function to check if a sector is erased (starts with 0xFF)
|
// Helper function to check if a sector is erased (starts with 0xFF)
|
||||||
static bool is_sector_erased(uint32_t x) {
|
static bool is_sector_erased(uint32_t x) {
|
||||||
uint8_t buf; //[256];
|
uint8_t buf;
|
||||||
esp_err_t err = esp_partition_read(storage_partition, LOG_START_OFFSET + x * FLASH_SECTOR_SIZE, &buf, 1);
|
esp_err_t err = esp_partition_read(log_partition, x * FLASH_SECTOR_SIZE, &buf, 1);
|
||||||
if (err != ESP_OK) return false;
|
if (err != ESP_OK) return false;
|
||||||
if (buf == 0xFF) return true;
|
return (buf == 0xFF);
|
||||||
|
|
||||||
/*for (int i = 0; i < 256; i++) {
|
|
||||||
if (buf[i] != 0xFF) return false;
|
|
||||||
}*/
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool is_sector_full(uint32_t x) {
|
static bool is_sector_full(uint32_t x) {
|
||||||
uint8_t buf; //[256];
|
uint8_t buf;
|
||||||
esp_err_t err = esp_partition_read(storage_partition, LOG_START_OFFSET + (x+1) * FLASH_SECTOR_SIZE - 1, &buf, 1);
|
esp_err_t err = esp_partition_read(log_partition, (x+1) * FLASH_SECTOR_SIZE - 1, &buf, 1);
|
||||||
if (err != ESP_OK) return false;
|
if (err != ESP_OK) return false;
|
||||||
if (buf == 0xFF) return false;
|
return (buf != 0xFF);
|
||||||
|
|
||||||
/*for (int i = 0; i < 256; i++) {
|
|
||||||
if (buf[i] != 0xFF) return false;
|
|
||||||
}*/
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void find_head_tail(int32_t num_sectors, int32_t *head, int32_t *tail) {
|
static inline void find_head_tail(int32_t num_sectors, int32_t *head, int32_t *tail) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to check if a sector has data (contains non-0xFF, non-0x00 bytes)
|
|
||||||
/*static bool sector_has_data(uint32_t sector_offset) {
|
|
||||||
uint8_t buf; //[256];
|
|
||||||
esp_err_t err = esp_partition_read(storage_partition, sector_offset, &buf, 256);
|
|
||||||
if (err != ESP_OK) return false;
|
|
||||||
if (buf )
|
|
||||||
|
|
||||||
for (int i = 0; i < 256; i++) {
|
|
||||||
if (buf[i] != 0xFF && buf[i] != 0x00) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// Replace log_write with this non-blocking version:
|
// Replace log_write with this non-blocking version:
|
||||||
esp_err_t log_write(uint8_t* buf, uint8_t len, uint8_t type) {
|
esp_err_t log_write(uint8_t* buf, uint8_t len, uint8_t type) {
|
||||||
if (!log_initialized || storage_partition == NULL) {
|
if (!log_initialized || log_partition == NULL) {
|
||||||
ESP_LOGE(TAG, "Logging not initialized");
|
ESP_LOGE(TAG, "Logging not initialized");
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
@@ -588,7 +762,7 @@ esp_err_t log_write(uint8_t* buf, uint8_t len, uint8_t type) {
|
|||||||
|
|
||||||
// The actual blocking write function (called by the task)
|
// The actual blocking write function (called by the task)
|
||||||
static esp_err_t log_write_blocking(uint8_t* buf, uint8_t len, uint8_t type) {
|
static esp_err_t log_write_blocking(uint8_t* buf, uint8_t len, uint8_t type) {
|
||||||
if (!log_initialized || storage_partition == NULL) {
|
if (!log_initialized || log_partition == NULL) {
|
||||||
ESP_LOGE(TAG, "Logging not initialized");
|
ESP_LOGE(TAG, "Logging not initialized");
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
@@ -602,27 +776,25 @@ static esp_err_t log_write_blocking(uint8_t* buf, uint8_t len, uint8_t type) {
|
|||||||
|
|
||||||
// check if we will overrun the sector
|
// check if we will overrun the sector
|
||||||
if (log_head_offset + len+2 >= log_sector_end(log_head_offset)) {
|
if (log_head_offset + len+2 >= log_sector_end(log_head_offset)) {
|
||||||
//ESP_LOGI(TAG, "WILL OVERRUN (%ld >= %ld)", (long)log_head_offset + len+2, (long)log_sector_end(log_head_offset));
|
|
||||||
// zero the rest of sector
|
// zero the rest of sector
|
||||||
char zeros[256] = {0};
|
char zeros[256] = {0};
|
||||||
esp_partition_write(storage_partition,
|
esp_partition_write(log_partition,
|
||||||
log_head_offset, &zeros,
|
log_head_offset, &zeros,
|
||||||
log_sector_end(log_head_offset)-log_head_offset);
|
log_sector_end(log_head_offset)-log_head_offset);
|
||||||
|
|
||||||
// set head to next sector, and check for wrap
|
// set head to next sector, and check for wrap
|
||||||
log_head_offset = log_sector_end(log_head_offset);
|
log_head_offset = log_sector_end(log_head_offset);
|
||||||
if (log_head_offset >= storage_partition->size)
|
if (log_head_offset >= log_partition->size)
|
||||||
log_head_offset = LOG_START_OFFSET;
|
log_head_offset = 0;
|
||||||
|
|
||||||
// Next write will be in a new sector - check if it needs erasing
|
// Next write will be in a new sector - check if it needs erasing
|
||||||
uint8_t check_byte;
|
uint8_t check_byte;
|
||||||
esp_err_t err = esp_partition_read(storage_partition, log_head_offset,
|
esp_err_t err = esp_partition_read(log_partition, log_head_offset,
|
||||||
&check_byte, 1);
|
&check_byte, 1);
|
||||||
|
|
||||||
// Erase the next sector
|
// Erase the next sector
|
||||||
if (err == ESP_OK && check_byte != 0xFF) {
|
if (err == ESP_OK && check_byte != 0xFF) {
|
||||||
//ESP_LOGI(TAG, "Erasing sector %lu for log", (unsigned long)log_head_offset);
|
err = esp_partition_erase_range(log_partition,
|
||||||
err = esp_partition_erase_range(storage_partition,
|
|
||||||
log_head_offset,
|
log_head_offset,
|
||||||
FLASH_SECTOR_SIZE);
|
FLASH_SECTOR_SIZE);
|
||||||
|
|
||||||
@@ -636,8 +808,8 @@ static esp_err_t log_write_blocking(uint8_t* buf, uint8_t len, uint8_t type) {
|
|||||||
|
|
||||||
// update the tail, if needed
|
// update the tail, if needed
|
||||||
if (log_tail_offset >= log_head_offset + FLASH_SECTOR_SIZE) log_tail_offset = log_head_offset + FLASH_SECTOR_SIZE;
|
if (log_tail_offset >= log_head_offset + FLASH_SECTOR_SIZE) log_tail_offset = log_head_offset + FLASH_SECTOR_SIZE;
|
||||||
if (log_tail_offset >= storage_partition->size)
|
if (log_tail_offset >= log_partition->size)
|
||||||
log_tail_offset = LOG_START_OFFSET;
|
log_tail_offset = 0;
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Erased; Tail/Head are now %lu/%lu",
|
ESP_LOGI(TAG, "Erased; Tail/Head are now %lu/%lu",
|
||||||
(unsigned long)log_tail_offset, (unsigned long)log_head_offset);
|
(unsigned long)log_tail_offset, (unsigned long)log_head_offset);
|
||||||
@@ -645,9 +817,9 @@ static esp_err_t log_write_blocking(uint8_t* buf, uint8_t len, uint8_t type) {
|
|||||||
len++; // account for type bit
|
len++; // account for type bit
|
||||||
|
|
||||||
|
|
||||||
esp_partition_write(storage_partition, log_head_offset, &len, 1);
|
esp_partition_write(log_partition, log_head_offset, &len, 1);
|
||||||
esp_partition_write(storage_partition, log_head_offset+1, buf, len-1);
|
esp_partition_write(log_partition, log_head_offset+1, buf, len-1);
|
||||||
esp_partition_write(storage_partition, log_head_offset+len, &type, 1);
|
esp_partition_write(log_partition, log_head_offset+len, &type, 1);
|
||||||
|
|
||||||
log_head_offset+=len+1;
|
log_head_offset+=len+1;
|
||||||
ESP_LOGI(TAG, "Wrote; Tail/Head are now %lu/%lu",
|
ESP_LOGI(TAG, "Wrote; Tail/Head are now %lu/%lu",
|
||||||
@@ -686,21 +858,29 @@ static void log_writer_task(void *pvParameters) {
|
|||||||
|
|
||||||
// Modified log_init to create queue and task
|
// Modified log_init to create queue and task
|
||||||
esp_err_t log_init() {
|
esp_err_t log_init() {
|
||||||
if (storage_partition == NULL) {
|
// Find the log partition
|
||||||
ESP_LOGE(TAG, "Storage partition not initialized, call storage_init() first");
|
log_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
|
||||||
return ESP_ERR_INVALID_STATE;
|
ESP_PARTITION_SUBTYPE_ANY,
|
||||||
|
"log");
|
||||||
|
if (log_partition == NULL) {
|
||||||
|
ESP_LOGE(TAG, "Log partition not found");
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log_mutex == NULL) {
|
||||||
|
log_mutex = xSemaphoreCreateMutex();
|
||||||
|
if (log_mutex == NULL) return ESP_ERR_NO_MEM;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
||||||
|
|
||||||
uint32_t log_area_size = storage_partition->size - LOG_START_OFFSET;
|
uint32_t num_sectors = log_partition->size / FLASH_SECTOR_SIZE;
|
||||||
uint32_t num_sectors = log_area_size / FLASH_SECTOR_SIZE;
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Log init: scanning %lu sectors with bisection", (unsigned long)num_sectors);
|
ESP_LOGI(TAG, "Log init: scanning %lu sectors with bisection", (unsigned long)num_sectors);
|
||||||
|
|
||||||
// Default to empty log
|
// Default to empty log
|
||||||
log_head_offset = LOG_START_OFFSET;
|
log_head_offset = 0;
|
||||||
log_tail_offset = LOG_START_OFFSET;
|
log_tail_offset = 0;
|
||||||
|
|
||||||
// Binary search for the first non-full sector
|
// Binary search for the first non-full sector
|
||||||
int32_t l = 0;
|
int32_t l = 0;
|
||||||
@@ -750,10 +930,10 @@ esp_err_t log_init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (head_sector == -1) {
|
if (head_sector == -1) {
|
||||||
if (is_sector_erased(LOG_START_OFFSET)) {
|
if (is_sector_erased(0)) {
|
||||||
ESP_LOGI(TAG, "Log is empty");
|
ESP_LOGI(TAG, "Log is empty");
|
||||||
log_head_offset = LOG_START_OFFSET;
|
log_head_offset = 0;
|
||||||
log_tail_offset = LOG_START_OFFSET;
|
log_tail_offset = 0;
|
||||||
} else {
|
} else {
|
||||||
head_sector = 0;
|
head_sector = 0;
|
||||||
ESP_LOGW(TAG, "Log appears full, searching from start");
|
ESP_LOGW(TAG, "Log appears full, searching from start");
|
||||||
@@ -763,17 +943,17 @@ esp_err_t log_init() {
|
|||||||
// Walk the data structure to find exact head
|
// Walk the data structure to find exact head
|
||||||
uint32_t cursor;
|
uint32_t cursor;
|
||||||
if (head_sector > 0) {
|
if (head_sector > 0) {
|
||||||
cursor = LOG_START_OFFSET + (head_sector - 1) * FLASH_SECTOR_SIZE;
|
cursor = (head_sector - 1) * FLASH_SECTOR_SIZE;
|
||||||
} else {
|
} else {
|
||||||
cursor = LOG_START_OFFSET;
|
cursor = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t head_sector_start = LOG_START_OFFSET + head_sector * FLASH_SECTOR_SIZE;
|
uint32_t head_sector_start = head_sector * FLASH_SECTOR_SIZE;
|
||||||
|
|
||||||
bool found_head = false;
|
bool found_head = false;
|
||||||
while (cursor < head_sector_start + FLASH_SECTOR_SIZE && cursor < storage_partition->size) {
|
while (cursor < head_sector_start + FLASH_SECTOR_SIZE && cursor < log_partition->size) {
|
||||||
uint8_t buf;
|
uint8_t buf;
|
||||||
esp_err_t err = esp_partition_read(storage_partition, cursor, &buf, 1);
|
esp_err_t err = esp_partition_read(log_partition, cursor, &buf, 1);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to read during log init at offset %lu", (unsigned long)cursor);
|
ESP_LOGE(TAG, "Failed to read during log init at offset %lu", (unsigned long)cursor);
|
||||||
if (log_mutex) xSemaphoreGive(log_mutex);
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
||||||
@@ -798,9 +978,9 @@ esp_err_t log_init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tail_sector >= 0) {
|
if (tail_sector >= 0) {
|
||||||
log_tail_offset = LOG_START_OFFSET + tail_sector * FLASH_SECTOR_SIZE;
|
log_tail_offset = tail_sector * FLASH_SECTOR_SIZE;
|
||||||
} else {
|
} else {
|
||||||
log_tail_offset = LOG_START_OFFSET;
|
log_tail_offset = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
log_initialized = true;
|
log_initialized = true;
|
||||||
@@ -866,7 +1046,9 @@ void storage_deinit(void) {
|
|||||||
log_queue = NULL;
|
log_queue = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
storage_partition = NULL;
|
params_partition = NULL;
|
||||||
|
log_partition = NULL;
|
||||||
|
post_partition = NULL;
|
||||||
log_initialized = false;
|
log_initialized = false;
|
||||||
if (log_mutex) {
|
if (log_mutex) {
|
||||||
vSemaphoreDelete(log_mutex);
|
vSemaphoreDelete(log_mutex);
|
||||||
@@ -879,54 +1061,12 @@ void storage_deinit(void) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
// ============================================================================
|
||||||
* ADDITIONS TO storage.c
|
// LOG READ / TEST SUPPORT FUNCTIONS
|
||||||
*
|
// ============================================================================
|
||||||
* Add these functions to storage.c to support the test suite.
|
|
||||||
* These provide the ability to erase all log sectors, simulate power cycles,
|
|
||||||
* and read back log entries for verification.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Add these function declarations to storage.h:
|
|
||||||
/*
|
|
||||||
esp_err_t log_read(uint8_t* len, uint8_t* buf, uint8_t* type);
|
|
||||||
void log_read_reset(void);
|
|
||||||
esp_err_t log_erase_all_sectors(void);
|
|
||||||
esp_err_t log_simulate_power_cycle(void);
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Add this static variable near the top of storage.c with other log static variables
|
|
||||||
// (around line 118, after log_tail_offset and log_initialized):
|
|
||||||
|
|
||||||
static uint32_t log_read_cursor = 0;
|
static uint32_t log_read_cursor = 0;
|
||||||
|
|
||||||
|
|
||||||
// Add these functions to storage.c (after the existing log functions):
|
|
||||||
|
|
||||||
/*
|
|
||||||
* ADDITIONS TO storage.c
|
|
||||||
*
|
|
||||||
* Add these functions to storage.c to support the test suite.
|
|
||||||
* These provide the ability to erase all log sectors, simulate power cycles,
|
|
||||||
* and read back log entries for verification.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Add these function declarations to storage.h:
|
|
||||||
/*
|
|
||||||
esp_err_t log_read(uint8_t* len, uint8_t* buf, uint8_t* type);
|
|
||||||
void log_read_reset(void);
|
|
||||||
esp_err_t log_erase_all_sectors(void);
|
|
||||||
esp_err_t log_simulate_power_cycle(void);
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Add this static variable near the top of storage.c with other log static variables
|
|
||||||
// (around line 118, after log_tail_offset and log_initialized):
|
|
||||||
/*
|
|
||||||
static uint32_t log_read_cursor = 0;
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Add these functions to storage.c (after the existing log functions):
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Read a log entry from the current read cursor position
|
* @brief Read a log entry from the current read cursor position
|
||||||
*
|
*
|
||||||
@@ -952,7 +1092,7 @@ esp_err_t log_read(uint8_t* len, uint8_t* buf, uint8_t* type) {
|
|||||||
// Add this declaration near the other log static variables in storage.c:
|
// Add this declaration near the other log static variables in storage.c:
|
||||||
// static uint32_t log_read_cursor = 0;
|
// static uint32_t log_read_cursor = 0;
|
||||||
|
|
||||||
if (!log_initialized || storage_partition == NULL) {
|
if (!log_initialized || log_partition == NULL) {
|
||||||
ESP_LOGE(TAG, "Logging not initialized");
|
ESP_LOGE(TAG, "Logging not initialized");
|
||||||
return ESP_ERR_INVALID_STATE;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
@@ -977,7 +1117,7 @@ esp_err_t log_read(uint8_t* len, uint8_t* buf, uint8_t* type) {
|
|||||||
|
|
||||||
// Read the length byte
|
// Read the length byte
|
||||||
uint8_t entry_len;
|
uint8_t entry_len;
|
||||||
esp_err_t err = esp_partition_read(storage_partition, log_read_cursor, &entry_len, 1);
|
esp_err_t err = esp_partition_read(log_partition, log_read_cursor, &entry_len, 1);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to read entry length at offset %lu", (unsigned long)log_read_cursor);
|
ESP_LOGE(TAG, "Failed to read entry length at offset %lu", (unsigned long)log_read_cursor);
|
||||||
if (log_mutex) xSemaphoreGive(log_mutex);
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
||||||
@@ -996,8 +1136,8 @@ esp_err_t log_read(uint8_t* len, uint8_t* buf, uint8_t* type) {
|
|||||||
uint32_t next_sector = ((log_read_cursor / FLASH_SECTOR_SIZE) + 1) * FLASH_SECTOR_SIZE;
|
uint32_t next_sector = ((log_read_cursor / FLASH_SECTOR_SIZE) + 1) * FLASH_SECTOR_SIZE;
|
||||||
|
|
||||||
// Handle wraparound
|
// Handle wraparound
|
||||||
if (next_sector >= storage_partition->size) {
|
if (next_sector >= log_partition->size) {
|
||||||
log_read_cursor = LOG_START_OFFSET;
|
log_read_cursor = 0;
|
||||||
} else {
|
} else {
|
||||||
log_read_cursor = next_sector;
|
log_read_cursor = next_sector;
|
||||||
}
|
}
|
||||||
@@ -1015,7 +1155,7 @@ esp_err_t log_read(uint8_t* len, uint8_t* buf, uint8_t* type) {
|
|||||||
|
|
||||||
// Read the data (length-1 bytes, since length includes the type byte)
|
// Read the data (length-1 bytes, since length includes the type byte)
|
||||||
uint8_t data_len = entry_len - 1;
|
uint8_t data_len = entry_len - 1;
|
||||||
err = esp_partition_read(storage_partition, log_read_cursor + 1, buf, data_len);
|
err = esp_partition_read(log_partition, log_read_cursor + 1, buf, data_len);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to read entry data at offset %lu", (unsigned long)(log_read_cursor + 1));
|
ESP_LOGE(TAG, "Failed to read entry data at offset %lu", (unsigned long)(log_read_cursor + 1));
|
||||||
if (log_mutex) xSemaphoreGive(log_mutex);
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
||||||
@@ -1024,7 +1164,7 @@ esp_err_t log_read(uint8_t* len, uint8_t* buf, uint8_t* type) {
|
|||||||
|
|
||||||
// Read the type byte
|
// Read the type byte
|
||||||
uint8_t entry_type;
|
uint8_t entry_type;
|
||||||
err = esp_partition_read(storage_partition, log_read_cursor + entry_len, &entry_type, 1);
|
err = esp_partition_read(log_partition, log_read_cursor + entry_len, &entry_type, 1);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to read entry type at offset %lu", (unsigned long)(log_read_cursor + entry_len));
|
ESP_LOGE(TAG, "Failed to read entry type at offset %lu", (unsigned long)(log_read_cursor + entry_len));
|
||||||
if (log_mutex) xSemaphoreGive(log_mutex);
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
||||||
@@ -1045,8 +1185,8 @@ esp_err_t log_read(uint8_t* len, uint8_t* buf, uint8_t* type) {
|
|||||||
log_read_cursor += entry_len + 1; // +1 for the type byte after data
|
log_read_cursor += entry_len + 1; // +1 for the type byte after data
|
||||||
|
|
||||||
// Handle wraparound
|
// Handle wraparound
|
||||||
if (log_read_cursor >= storage_partition->size) {
|
if (log_read_cursor >= log_partition->size) {
|
||||||
log_read_cursor = LOG_START_OFFSET;
|
log_read_cursor = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log_mutex) xSemaphoreGive(log_mutex);
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
||||||
@@ -1078,8 +1218,8 @@ void log_read_reset(void) {
|
|||||||
* @return ESP_OK on success, error code otherwise
|
* @return ESP_OK on success, error code otherwise
|
||||||
*/
|
*/
|
||||||
esp_err_t log_erase_all_sectors(void) {
|
esp_err_t log_erase_all_sectors(void) {
|
||||||
if (storage_partition == NULL) {
|
if (log_partition == NULL) {
|
||||||
ESP_LOGE(TAG, "Storage partition not initialized");
|
ESP_LOGE(TAG, "Log partition not initialized");
|
||||||
return ESP_ERR_INVALID_STATE;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
// Stop the log writer task
|
// Stop the log writer task
|
||||||
@@ -1088,8 +1228,6 @@ esp_err_t log_erase_all_sectors(void) {
|
|||||||
vTaskDelay(pdMS_TO_TICKS(200));
|
vTaskDelay(pdMS_TO_TICKS(200));
|
||||||
|
|
||||||
if (log_task_handle != NULL) {
|
if (log_task_handle != NULL) {
|
||||||
// Don't try to delete from watchdog - task was never added
|
|
||||||
// esp_task_wdt_delete(log_task_handle); // <-- REMOVE THIS LINE
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(100));
|
vTaskDelay(pdMS_TO_TICKS(100));
|
||||||
log_task_handle = NULL;
|
log_task_handle = NULL;
|
||||||
}
|
}
|
||||||
@@ -1102,13 +1240,9 @@ esp_err_t log_erase_all_sectors(void) {
|
|||||||
|
|
||||||
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
||||||
|
|
||||||
uint32_t log_area_size = storage_partition->size - LOG_START_OFFSET;
|
ESP_LOGI(TAG, "Erasing all log sectors (%lu bytes)...", (unsigned long)log_partition->size);
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Erasing all log sectors (%lu bytes)...", (unsigned long)log_area_size);
|
esp_err_t err = esp_partition_erase_range(log_partition, 0, log_partition->size);
|
||||||
|
|
||||||
esp_err_t err = esp_partition_erase_range(storage_partition,
|
|
||||||
LOG_START_OFFSET,
|
|
||||||
log_area_size);
|
|
||||||
|
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to erase log area: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "Failed to erase log area: %s", esp_err_to_name(err));
|
||||||
|
|||||||
108
main/storage.h
108
main/storage.h
@@ -10,9 +10,6 @@
|
|||||||
// FLASH LAYOUT CONSTANTS
|
// FLASH LAYOUT CONSTANTS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
#define FLASH_SECTOR_SIZE 4096
|
#define FLASH_SECTOR_SIZE 4096
|
||||||
#define PARAMS_OFFSET 0
|
|
||||||
#define PARAMETER_NUM_SECTORS 4
|
|
||||||
#define LOG_START_OFFSET FLASH_SECTOR_SIZE*PARAMETER_NUM_SECTORS // Start after first sector (parameters)
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// LOG ENTRY TYPE DEFINITIONS (Magic values 0xC0-0xCF)
|
// LOG ENTRY TYPE DEFINITIONS (Magic values 0xC0-0xCF)
|
||||||
@@ -54,61 +51,63 @@ typedef struct {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
|
||||||
// TODO: Bounds checking / constraints (especially no division by zero, no NaNs, no infs)
|
// PARAM_DEF(name, type, default, unit, min, max)
|
||||||
// TODO: abandoned parameters (esp. jack current)
|
// min == max → skip bounds validation (used for keycodes, strings, informational params)
|
||||||
|
// Division-critical params have min > 0 to prevent div-by-zero
|
||||||
|
// NaN/Inf floats are always reset to default regardless of bounds
|
||||||
|
|
||||||
#define PARAM_LIST \
|
#define PARAM_LIST \
|
||||||
PARAM_DEF(BOOT_TIME, i32, 0, "us") \
|
PARAM_DEF(BOOT_TIME, i32, 0, "us", 0, 0) /* informational, skip */ \
|
||||||
PARAM_DEF(NUM_MOVES, u32, 0, "") \
|
PARAM_DEF(NUM_MOVES, u32, 0, "", 0, 1000) \
|
||||||
PARAM_DEF(MOVE_START, u32, 0, "s") \
|
PARAM_DEF(MOVE_START, u32, 0, "s", 0, 86400) \
|
||||||
PARAM_DEF(MOVE_END, u32, 0, "s") \
|
PARAM_DEF(MOVE_END, u32, 0, "s", 0, 86400) \
|
||||||
PARAM_DEF(DRIVE_DIST, f32, 10, "ft") \
|
PARAM_DEF(DRIVE_DIST, f32, 10, "ft", 0.0, 100.0) \
|
||||||
PARAM_DEF(JACK_DIST, f32, 5, "in") \
|
PARAM_DEF(JACK_DIST, f32, 5, "in", 0.0, 10.0) \
|
||||||
PARAM_DEF(DRIVE_KE, f32, 29.2, "n/ft") \
|
PARAM_DEF(DRIVE_KE, f32, 29.2, "n/ft", 1.0, 1e9) \
|
||||||
PARAM_DEF(DRIVE_KT, f32, 2880000, "us/ft") \
|
PARAM_DEF(DRIVE_KT, f32, 2880000, "us/ft", 1.0, 1e9) /* div-critical */ \
|
||||||
PARAM_DEF(JACK_KT, f32, 1428571, "ms/in") \
|
PARAM_DEF(JACK_KT, f32, 1428571, "ms/in", 1.0, 1e9) /* div-critical */ \
|
||||||
PARAM_DEF(KEYCODE_0, u32, 0, "") \
|
PARAM_DEF(KEYCODE_0, u32, 0, "", 0, 0) /* skip */ \
|
||||||
PARAM_DEF(KEYCODE_1, u32, 0, "") \
|
PARAM_DEF(KEYCODE_1, u32, 0, "", 0, 0) \
|
||||||
PARAM_DEF(KEYCODE_2, u32, 0, "") \
|
PARAM_DEF(KEYCODE_2, u32, 0, "", 0, 0) \
|
||||||
PARAM_DEF(KEYCODE_3, u32, 0, "") \
|
PARAM_DEF(KEYCODE_3, u32, 0, "", 0, 0) \
|
||||||
PARAM_DEF(KEYCODE_4, u32, 0, "") \
|
PARAM_DEF(KEYCODE_4, u32, 0, "", 0, 0) \
|
||||||
PARAM_DEF(KEYCODE_5, u32, 0, "") \
|
PARAM_DEF(KEYCODE_5, u32, 0, "", 0, 0) \
|
||||||
PARAM_DEF(KEYCODE_6, u32, 0, "") \
|
PARAM_DEF(KEYCODE_6, u32, 0, "", 0, 0) \
|
||||||
PARAM_DEF(KEYCODE_7, u32, 0, "") \
|
PARAM_DEF(KEYCODE_7, u32, 0, "", 0, 0) \
|
||||||
PARAM_DEF(ADC_ALPHA_BATTERY, f32, 0.5, "-") \
|
PARAM_DEF(ADC_ALPHA_BATTERY, f32, 0.5, "-", 0.0, 1.0) \
|
||||||
PARAM_DEF(ADC_ALPHA_ISENS, f32, 0.6, "-") \
|
PARAM_DEF(ADC_ALPHA_ISENS, f32, 0.6, "-", 0.0, 1.0) \
|
||||||
PARAM_DEF(ADC_ALPHA_IAZ, f32, 0.005, "-") \
|
PARAM_DEF(ADC_ALPHA_IAZ, f32, 0.005, "-", 0.0, 1.0) \
|
||||||
PARAM_DEF(ADC_DB_IAZ, f32, 5.0, "A") \
|
PARAM_DEF(ADC_DB_IAZ, f32, 5.0, "A", 0.0, 200.0) \
|
||||||
PARAM_DEF(EFUSE_INOM_1, f32, 40.0, "A") \
|
PARAM_DEF(EFUSE_INOM_1, f32, 40.0, "A", 0.0, 200.0) \
|
||||||
PARAM_DEF(EFUSE_INOM_2, f32, 14.0, "A") \
|
PARAM_DEF(EFUSE_INOM_2, f32, 14.0, "A", 0.0, 200.0) \
|
||||||
PARAM_DEF(EFUSE_INOM_3, f32, 4.0, "A") \
|
PARAM_DEF(EFUSE_INOM_3, f32, 4.0, "A", 0.0, 200.0) \
|
||||||
PARAM_DEF(EFUSE_HEAT_THRESH, f32, 60.0, "i/i^2-s") \
|
PARAM_DEF(EFUSE_HEAT_THRESH, f32, 60.0, "i/i^2-s", 0.0, 1e9) \
|
||||||
PARAM_DEF(EFUSE_KINST, f32, 2.0, "i/i") \
|
PARAM_DEF(EFUSE_KINST, f32, 2.0, "i/i", 0.01, 100.0) /* div-critical */ \
|
||||||
PARAM_DEF(EFUSE_TAUCOOL, f32, 0.2, "i") \
|
PARAM_DEF(EFUSE_TAUCOOL, f32, 0.2, "i", 0.0, 100.0) \
|
||||||
PARAM_DEF(EFUSE_TCOOL, u32, 5000000, "us") \
|
PARAM_DEF(EFUSE_TCOOL, u32, 5000000, "us", 0, 60000000) \
|
||||||
PARAM_DEF(LOW_PROTECTION_V, f32, 10.0, "V") \
|
PARAM_DEF(LOW_PROTECTION_V, f32, 10.0, "V", 0.0, 100.0) \
|
||||||
PARAM_DEF(LOW_PROTECTION_S, u32, 10, "s") \
|
PARAM_DEF(LOW_PROTECTION_S, u32, 10, "s", 0, 3600) \
|
||||||
PARAM_DEF(CHG_LOW_V, f32, 5.0, "V") \
|
PARAM_DEF(CHG_LOW_V, f32, 5.0, "V", 0.0, 100.0) \
|
||||||
PARAM_DEF(CHG_LOW_S, u32, 5, "s") \
|
PARAM_DEF(CHG_LOW_S, u32, 5, "s", 0, 3600) \
|
||||||
PARAM_DEF(CHG_BULK_S, u32, 20, "s") \
|
PARAM_DEF(CHG_BULK_S, u32, 20, "s", 0, 3600) \
|
||||||
PARAM_DEF(RF_PULSE_LENGTH, u32, 350000, "us") \
|
PARAM_DEF(RF_PULSE_LENGTH, u32, 350000, "us", 0, 10000000) \
|
||||||
PARAM_DEF(V_SENS_OFFSET, f32, 0.4, "V") \
|
PARAM_DEF(V_SENS_OFFSET, f32, 0.4, "V", -10.0, 10.0) \
|
||||||
PARAM_DEF(NET_SSID, str, "", "") \
|
PARAM_DEF(NET_SSID, str, "", "", "", "") \
|
||||||
PARAM_DEF(NET_PASS, str, "", "") \
|
PARAM_DEF(NET_PASS, str, "", "", "", "") \
|
||||||
PARAM_DEF(WIFI_CHANNEL, u16, 6, "") \
|
PARAM_DEF(WIFI_CHANNEL, u16, 6, "", 1, 14) \
|
||||||
PARAM_DEF(WIFI_SSID, str, "sc.local", "") \
|
PARAM_DEF(WIFI_SSID, str, "sc.local", "", "", "") \
|
||||||
PARAM_DEF(WIFI_PASS, str, "password", "") \
|
PARAM_DEF(WIFI_PASS, str, "password", "", "", "") \
|
||||||
PARAM_DEF(EFUSE_INRUSH_US, u32, 250000, "us") \
|
PARAM_DEF(EFUSE_INRUSH_US, u32, 250000, "us", 0, 10000000) \
|
||||||
PARAM_DEF(JACK_I_UP, f32, 8.0, "A") \
|
PARAM_DEF(JACK_I_UP, f32, 8.0, "A", 0.0, 200.0) \
|
||||||
PARAM_DEF(JACK_I_DOWN, f32, 15.0, "A") \
|
PARAM_DEF(JACK_I_DOWN, f32, 15.0, "A", 0.0, 200.0) \
|
||||||
PARAM_DEF(V_SENS_K, f32, 0.00766666666, "V/mV") \
|
PARAM_DEF(V_SENS_K, f32, 0.00766666666, "V/mV", 0.0, 1.0) \
|
||||||
PARAM_DEF(BUILD_VERSION, str, "undefined", "") \
|
PARAM_DEF(BUILD_VERSION, str, "undefined", "", "", "") \
|
||||||
PARAM_DEF(SAFETY_BREAK_US, u32, 300000, "") \
|
PARAM_DEF(SAFETY_BREAK_US, u32, 300000, "", 0, 10000000) \
|
||||||
PARAM_DEF(SAFETY_MAKE_US, u32, 1000000, "") \
|
PARAM_DEF(SAFETY_MAKE_US, u32, 1000000, "", 0, 10000000) \
|
||||||
PARAM_DEF(JACK_IS_DOWN, f32, 8.0, "A") \
|
PARAM_DEF(JACK_IS_DOWN, f32, 8.0, "A", 0.0, 200.0) /* deprecated: may duplicate JACK_I_DOWN */
|
||||||
|
|
||||||
// Generate enum for parameter indices
|
// Generate enum for parameter indices
|
||||||
#define PARAM_DEF(name, type, default_val, unit) PARAM_##name,
|
#define PARAM_DEF(name, type, default_val, unit, min, max) PARAM_##name,
|
||||||
typedef enum {
|
typedef enum {
|
||||||
PARAM_LIST
|
PARAM_LIST
|
||||||
NUM_PARAMS
|
NUM_PARAMS
|
||||||
@@ -153,6 +152,7 @@ typedef struct {
|
|||||||
|
|
||||||
// Initialization
|
// Initialization
|
||||||
esp_err_t storage_init(void);
|
esp_err_t storage_init(void);
|
||||||
|
esp_err_t storage_post(void);
|
||||||
void storage_deinit(void);
|
void storage_deinit(void);
|
||||||
|
|
||||||
// Parameter access
|
// Parameter access
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
const unsigned char PROGMEM html_content_gz[] = {
|
const unsigned char PROGMEM html_content_gz[] = {
|
||||||
0x1f, 0x8b, 0x08, 0x00, 0xdf, 0x91, 0xb1, 0x69, 0x02, 0xff, 0xed, 0x3d, 0xfb, 0x5b, 0xdb, 0x48,
|
0x1f, 0x8b, 0x08, 0x00, 0x18, 0x55, 0xb3, 0x69, 0x02, 0xff, 0xed, 0x3d, 0xfb, 0x5b, 0xdb, 0x48,
|
||||||
0x92, 0x3f, 0xef, 0xfc, 0x15, 0x0d, 0x93, 0x21, 0x52, 0x10, 0xb2, 0x0d, 0x64, 0x66, 0xd6, 0x46,
|
0x92, 0x3f, 0xef, 0xfc, 0x15, 0x0d, 0x93, 0x21, 0x52, 0x10, 0xb2, 0x0d, 0x64, 0x66, 0xd6, 0x46,
|
||||||
0x66, 0x09, 0x38, 0x3b, 0x4c, 0x12, 0xe0, 0xc3, 0x90, 0xcc, 0x1c, 0xc7, 0x87, 0x64, 0xab, 0x8d,
|
0x66, 0x09, 0x38, 0x3b, 0x4c, 0x12, 0xe0, 0xc3, 0x90, 0xcc, 0x1c, 0xc7, 0x87, 0x64, 0xab, 0x8d,
|
||||||
0x35, 0xc8, 0x92, 0x57, 0x92, 0x21, 0x5e, 0xe3, 0xff, 0xfd, 0xaa, 0xfa, 0x21, 0xb5, 0x1e, 0x36,
|
0x35, 0xc8, 0x92, 0x57, 0x92, 0x21, 0x5e, 0xe3, 0xff, 0xfd, 0xaa, 0xfa, 0x21, 0xb5, 0x1e, 0x36,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
|
|
||||||
#include "webpage.h"
|
#include "webpage.h"
|
||||||
#include "webserver.h"
|
#include "webserver.h"
|
||||||
|
#include "comms_events.h"
|
||||||
|
|
||||||
#include "esp_partition.h"
|
#include "esp_partition.h"
|
||||||
|
|
||||||
@@ -99,7 +100,7 @@ static esp_err_t root_get_handler(httpd_req_t *req) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache the storage partition pointer to avoid repeated lookups
|
// Cache the storage partition pointer to avoid repeated lookups
|
||||||
static const esp_partition_t *cached_storage_partition = NULL;
|
static const esp_partition_t *cached_log_partition = NULL;
|
||||||
|
|
||||||
// In webserver.c - Replace the log_handler function
|
// In webserver.c - Replace the log_handler function
|
||||||
|
|
||||||
@@ -144,17 +145,17 @@ static esp_err_t log_handler(httpd_req_t *req) {
|
|||||||
return httpd_resp_send_err(req, HTTPD_405_METHOD_NOT_ALLOWED, "Method not allowed");
|
return httpd_resp_send_err(req, HTTPD_405_METHOD_NOT_ALLOWED, "Method not allowed");
|
||||||
}
|
}
|
||||||
|
|
||||||
const esp_partition_t *storage_partition = cached_storage_partition;
|
const esp_partition_t *log_part = cached_log_partition;
|
||||||
if (storage_partition == NULL) {
|
if (log_part == NULL) {
|
||||||
storage_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
|
log_part = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
|
||||||
ESP_PARTITION_SUBTYPE_ANY,
|
ESP_PARTITION_SUBTYPE_ANY,
|
||||||
"storage");
|
"log");
|
||||||
if (storage_partition == NULL) {
|
if (log_part == NULL) {
|
||||||
ESP_LOGE(TAG, "Storage partition not found");
|
ESP_LOGE(TAG, "Log partition not found");
|
||||||
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||||
"Storage partition not found");
|
"Log partition not found");
|
||||||
}
|
}
|
||||||
cached_storage_partition = storage_partition;
|
cached_log_partition = log_part;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t head = log_get_head();
|
int32_t head = log_get_head();
|
||||||
@@ -163,7 +164,7 @@ static esp_err_t log_handler(httpd_req_t *req) {
|
|||||||
if (tail < 0) {
|
if (tail < 0) {
|
||||||
tail = log_get_tail();
|
tail = log_get_tail();
|
||||||
} else {
|
} else {
|
||||||
if (tail < log_start || tail >= (int32_t)storage_partition->size) {
|
if (tail < log_start || tail >= (int32_t)log_part->size) {
|
||||||
ESP_LOGW(TAG, "Invalid tail pointer %ld, using current tail", (long)tail);
|
ESP_LOGW(TAG, "Invalid tail pointer %ld, using current tail", (long)tail);
|
||||||
tail = log_get_tail();
|
tail = log_get_tail();
|
||||||
}
|
}
|
||||||
@@ -176,7 +177,7 @@ static esp_err_t log_handler(httpd_req_t *req) {
|
|||||||
} else if (tail < head) {
|
} else if (tail < head) {
|
||||||
log_data_size = head - tail;
|
log_data_size = head - tail;
|
||||||
} else {
|
} else {
|
||||||
log_data_size = (storage_partition->size - tail) + (head - log_start);
|
log_data_size = (log_part->size - tail) + (head - log_start);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate JSON header (same as /get endpoint)
|
// Generate JSON header (same as /get endpoint)
|
||||||
@@ -270,7 +271,7 @@ static esp_err_t log_handler(httpd_req_t *req) {
|
|||||||
// Normal case: tail before head
|
// Normal case: tail before head
|
||||||
while (offset < head) {
|
while (offset < head) {
|
||||||
size_t to_read = MIN(sizeof(http_buffer), head - offset);
|
size_t to_read = MIN(sizeof(http_buffer), head - offset);
|
||||||
err = esp_partition_read(storage_partition, offset, http_buffer, to_read);
|
err = esp_partition_read(log_part, offset, http_buffer, to_read);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to read partition at offset %ld: %s",
|
ESP_LOGE(TAG, "Failed to read partition at offset %ld: %s",
|
||||||
(long)offset, esp_err_to_name(err));
|
(long)offset, esp_err_to_name(err));
|
||||||
@@ -290,9 +291,9 @@ static esp_err_t log_handler(httpd_req_t *req) {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Wrapped case: tail after head, read from tail to end, then start to head
|
// Wrapped case: tail after head, read from tail to end, then start to head
|
||||||
while (offset < (int32_t)storage_partition->size) {
|
while (offset < (int32_t)log_part->size) {
|
||||||
size_t to_read = MIN(sizeof(http_buffer), storage_partition->size - offset);
|
size_t to_read = MIN(sizeof(http_buffer), log_part->size - offset);
|
||||||
err = esp_partition_read(storage_partition, offset, http_buffer, to_read);
|
err = esp_partition_read(log_part, offset, http_buffer, to_read);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to read partition at offset %ld: %s",
|
ESP_LOGE(TAG, "Failed to read partition at offset %ld: %s",
|
||||||
(long)offset, esp_err_to_name(err));
|
(long)offset, esp_err_to_name(err));
|
||||||
@@ -314,7 +315,7 @@ static esp_err_t log_handler(httpd_req_t *req) {
|
|||||||
offset = log_start;
|
offset = log_start;
|
||||||
while (offset < head) {
|
while (offset < head) {
|
||||||
size_t to_read = MIN(sizeof(http_buffer), head - offset);
|
size_t to_read = MIN(sizeof(http_buffer), head - offset);
|
||||||
err = esp_partition_read(storage_partition, offset, http_buffer, to_read);
|
err = esp_partition_read(log_part, offset, http_buffer, to_read);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Failed to read partition at offset %ld: %s",
|
ESP_LOGE(TAG, "Failed to read partition at offset %ld: %s",
|
||||||
(long)offset, esp_err_to_name(err));
|
(long)offset, esp_err_to_name(err));
|
||||||
@@ -1017,6 +1018,7 @@ static esp_err_t try_connect_sta(const char *ssid, const char *pass, bool reset_
|
|||||||
}
|
}
|
||||||
|
|
||||||
s_wifi_running = true;
|
s_wifi_running = true;
|
||||||
|
if (comms_event_group) xEventGroupSetBits(comms_event_group, WIFI_READY_BIT);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1099,6 +1101,7 @@ static esp_err_t launch_soft_ap(void) {
|
|||||||
ESP_LOGI(TAG, "SoftAP ready. SSID: %s, Channel: %d, Password: %s",
|
ESP_LOGI(TAG, "SoftAP ready. SSID: %s, Channel: %d, Password: %s",
|
||||||
wifi_config.ap.ssid, wifi_config.ap.channel, placeholder);
|
wifi_config.ap.ssid, wifi_config.ap.channel, placeholder);
|
||||||
ESP_LOGI(TAG, "Access at: http://%s.local or http://192.168.4.1", HOSTNAME);
|
ESP_LOGI(TAG, "Access at: http://%s.local or http://192.168.4.1", HOSTNAME);
|
||||||
|
if (comms_event_group) xEventGroupSetBits(comms_event_group, WIFI_READY_BIT);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1124,6 +1127,7 @@ esp_err_t webserver_stop(void) {
|
|||||||
esp_wifi_stop();
|
esp_wifi_stop();
|
||||||
s_wifi_running = false;
|
s_wifi_running = false;
|
||||||
}
|
}
|
||||||
|
if (comms_event_group) xEventGroupClearBits(comms_event_group, WIFI_READY_BIT);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user