Files
SC-F001/README.md
Thaddeus Hughes 20afd3d9ef stashing
2026-06-10 16:40:27 -05:00

17 KiB
Raw Blame History

SC-F001 Firmware

Solar-powered autonomous livestock shelter mover built on the ESP32. Drives horizontally via a motor, lifts/lowers a the structure via a jack motor, with an auxiliary "fluffer" motor always running while driving. The firmware handles motor sequencing, safety interlocks, remote control, data logging, and a WiFi web interface.


Hardware Platform

MCU: ESP32 (Xtensa dual-core), ESP-IDF framework

GPIO Map:

GPIO Function
13 Button interrupt (active low, pull-up)
14 Jack position sensor
16 Not Used
19 Drive encoder
21/22 I2C SDA/SCL (400kHz) → TCA9555 I/O expander
25 433MHz RF receiver (RMT input)
26 Solar charger bulk enable (RTC GPIO)
27 Safety sensor (active low)
32/33 External 32.768 kHz RTC crystal (on PCB, not used — see RTC section)
34 ADC: Current Sensor
35 ADC: Battery Voltage
36 (VP) ADC: Current Sensor VOC
39 (VN) ADC: Current Sensor FAULT

TCA9555 (I2C at 0x21):

  • Port 0 (input): 2 physical buttons + 2 additional inputs + LEDs

  • Port 1 (output): 3× H-bridge relay pairs (DRIVE, JACK, AUX)

  • P00: SW1 (has external 4.7kOhm pullup)

  • P01: SW2 (not populated on SC-B001-V5)

  • P02-P04: N/C

  • P05-P07: LEDs (through 100ohm resistors)

  • P10: Sensor enable (1=ENABLE, 0=DISABLE)

  • P11: KC3 (not connected)

  • P12: KB3 (not connected)

  • P13: KA3 (aux relay)

  • P14: KB2 (jack B)

  • P15: KA2 (jack A)

  • P16: KB1 (drive B)

  • P17: KA1 (drive A)

All power goes through a ACS37220LEZATR-100B3 sense chip (13.2 mV/A)


Software Architecture

app_main()
├── i2c_init()              TCA9555 init (relays off, LEDs off)
├── rtc_xtal_init()         Button GPIO setup
├── boot_reset_reason       Check boot reason for factory reset
├── adc_init()              ADC1 calibration (12dB attenuation, line-fit)
├── storage_init()          Flash params
├── log_init()              Circular log buffer
├── adc_post()
├── storage_post()
├── solar_run_fsm()         (called in main loop too)
├── uart_init()             Serial JSON API task
├── sensors_init()          GPIO ISR setup for sensors/encoders
├── fsm_init()              Control FSM task (priority 10, 20ms tick)
├── rf_433_init()           433MHz RMT receiver task
├── bt_hid_init()           BLE HID host scanner task
└── webserver_init()        WiFi softAP + HTTP + mDNS + DNS

Main loop (50ms):
    soft-idle check
    button hold-to-reboot
    triple-tap detection
    alarm detection
    periodic send_bat_log
    i2c_poll_buttons()
    fsm_request() based on button events
    solar_run_fsm()
    drive_leds() status animation
    rtc_check_shutdown_timer() → soft idle on inactivity (180s)
    esp_task_wdt_reset()

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 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, soft idle, inactivity timer
solar.c/h Simple FLOAT/BULK solar charge state machine
sc_err.h Error code definitions
log_test.c/h Flash log unit tests

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_FLUFF_START
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, run until e-fuse/sensor)
→ back to STATE_IDLE

STATE_CALIBRATE_JACK_DELAY  / STATE_CALIBRATE_JACK_MOVE   (jack calibration sequence)
STATE_CALIBRATE_DRIVE_DELAY / STATE_CALIBRATE_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. drive_relays() — write relay output from current state
  6. send_fsm_log() — 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
/post POST JSON commands + parameter updates
/log ANY Binary log download (4B JSON len + JSON + 8B offsets + log data)
/ota POST Firmware update upload

UART (115200 8N1)

  • GET → same as HTTP GET /get
  • POST: {json} → same as HTTP POST /post
  • RTCDEBUG → dump RTC timekeeping state (time, backup, sleep entry, clock source)
  • 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 partitions (8MB flash):

Partition Offset Size Purpose
nvs 0x9000 16K WiFi/BT config, board revision, RTC time backup
otadata 0xD000 8K OTA boot selection
phy_init 0xF000 4K RF calibration data
ota_0 0x10000 1984K Factory / primary app slot
ota_1 0x200000 1984K OTA update slot
post_test 0x3F0000 4K Power-on self-test scratch sector
params 0x3F1000 32K CRC32-protected parameter storage (49 params)
log 0x400000 4096K Circular binary log buffer (head/tail tracked)

Log entry format (25 bytes typical):

[0:8]   ts_ms (u64)
[8:12]  bat_V (f32)
[12:16] current_A (f32)   — combined, not per-bridge
[16:18] counter (i16)
[18:19] sensors (u8)
[19:23] heat (f32)        — max across bridges
[23:25] i2c_out (u16)

RTC & Timekeeping

Time source: esp_timer (40 MHz APB crystal, ~20 ppm accuracy). The external 32.768 kHz crystal on GPIO32/33 is present on the PCB but not used — deep sleep is not used normally (soft idle instead), so RTC slow clock accuracy is irrelevant. The RTC slow clock uses the default internal RC oscillator.

rtc_xtal_init() in rtc.c: Configures the button GPIO (GPIO13); no crystal bootstrap or sleep wakeup sources.

Time persistence across resets: rtc_save_time() writes the current unix timestamp to NVS (namespace "hw", key "rtc_time"). On boot, rtc_restore_time() tries RTC_DATA_ATTR first, then falls back to NVS. This ensures time survives software resets even when the bootloader reloads RTC slow memory. The saved time will be stale by the reboot duration (~2s), which is acceptable.

Diagnosing time issues: Run RTCDEBUG over UART. Reports current time, sync time, elapsed since sync, next alarm, uptime, and soft idle state.


Button & LED Behavior

Single physical button (button 0 via TCA9555 I2C expander) controls all interactions. All logic lives in the main loop (50ms tick) in main.c.

Button Actions by State

IDLE — Triple-tap to start:

  • 3 taps within a 2-second window triggers FSM_CMD_START
  • Start fires immediately on the 3rd tap
  • LED feedback: 1 tap → LED 1, 2 taps → LED 1+2, 3 taps → LED 1+2+3 (then start)
  • LEDs persist until next tap or window expiry; counter resets on expiry

IDLE / CALIBRATE — 3-second hold to reboot:

  • Saves RTC time to NVS, then calls esp_restart()
  • LED progression: off (0750ms) → LED 1 (7501500ms) → LED 1+2 (15002250ms) → LED 1+2+3 (22503000ms) → flash all (6× at 150ms) → reboot

Moving states — any tap sends FSM_CMD_UNDO

UNDO state (UNDO_JACK_START) — any tap sends FSM_CMD_STOP (emergency stop)

Calibration states — tap advances through calibration steps (unchanged)

Factory reset — power cycle with GPIO13 held for 10 seconds. Resets all params and erases log/post_test partitions. Preserves NVS (board_rev, BT pairing, RTC time). Only triggers on ESP_RST_POWERON or ESP_RST_EXT.

LED Status Indicators

Physical LED layout — the three LEDs are wired to TCA9555 port-0 pins P05, P06, P07. Read bottom → top when checking error codes:

TCA pin Bit Physical position Called
P05 0 bottom LED1
P06 1 middle LED2
P07 2 top LED3

A pattern written as 001 (LSB first) means only the bottom LED is lit, 100 means only the top LED is lit, and 111 means all three.

State Pattern Timing
Idle LED1 blink 0.5Hz (1s on / 1s off)
Error Rapid all-blink → error code hold 5Hz for 1s, then code for 2s (3s cycle)
Moving / delays Waterfall 001→011→111→110→100→000 ~1 cycle/s (167ms per step)
Calibrating All LEDs flash 1Hz (500ms on / 500ms off)
Undo All LEDs solid on Continuous
Booting LED1 solid Until init complete

Error code bits (during 2s hold phase):

LED Pattern (bottom→top) Meaning
001 — only bottom (P05) lit Efuse tripped (any bridge) or low battery
010 — only middle (P06) lit RTC/clock not set
100 — only top (P07) lit Safety sensor break or leash limit hit
111 — all three lit Unknown FSM error (fallback)

Error codes are also shown on the web interface status field with individual flag names.

Implementation Details

  • Tap detection uses release edge (i2c_get_button_released()) with btn_held < 1000ms guard (long presses don't count as taps)
  • 2-second tap window starts on first tap, fixed duration (not reset by subsequent taps)
  • All button state sampled once per tick: btn_pressed, btn_tripped, btn_released, btn_held

Power Management

  • Battery voltage: GPIO35, thru divider → V = raw × V_SENS_K + V_SENS_OFFSET (defaults: K=0.00766̄, offset=0.4)
  • Solar charger: GPIO26 (RTC hold) — FLOAT/BULK FSM, bulk for 20s when V < 5V for 5s
  • Inactivity shutdown: 180s → soft idle (WiFi/BT off, LEDs off — not deep sleep). Button press exits soft idle.
  • RTC_DATA_ATTR: Sync timestamps, alarm times, charge state — survive software resets (panics, WDT)

Error Codes (sc_err.h)

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 (>=5.0)
  • Component deps (main/idf_component.yml): espressif/mdns ~1.9.1
  • 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.htmlwebpage_compile.pywebpage_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