Skip to content

Improve radar polling reliability and clean up unused variables#3

Open
daniel-frenkel wants to merge 11 commits intomainfrom
claude/review-esp32-v1.2-Rbv4w
Open

Improve radar polling reliability and clean up unused variables#3
daniel-frenkel wants to merge 11 commits intomainfrom
claude/review-esp32-v1.2-Rbv4w

Conversation

@daniel-frenkel
Copy link
Copy Markdown
Member

Summary

This PR improves the reliability of speed data collection from the STM32 radar sensor and performs significant cleanup of unused variables and reserved flags throughout the codebase.

Key Changes

Radar Polling Improvements:

  • Fixed get_speed() to drain stale bytes from the serial buffer before issuing new commands, preventing data lag where responses were always one cycle behind
  • Changed from polling-based available() check to blocking parseFloat() with a 50ms timeout, ensuring complete responses are received
  • Updated documentation to clarify the new behavior and timeout semantics
  • Changed speed variable type from int to float to preserve the 0.1 resolution provided by the STM32

Variable Cleanup:

  • Removed unused/reserved flags: API_photo, send_API, photo_finished, send_alert, sleep_idle_time
  • Removed unused ESPUI handles: labelWifi, button1, switchOne, status
  • Removed unused variables: lastTime, display_wifi, apIP, DNS_PORT constants
  • Changed maxSpeed from int to float to match STM32's 0.1 resolution and updated documentation
  • Moved dnsServer declaration from .ino to variables.h for better organization

Code Quality Improvements:

  • Fixed WiFi reset to use "NOT_SET" sentinel value consistently with other credential reset paths
  • Added DNS server initialization in AP mode with explanatory comments
  • Fixed buffer reservation bug in sendPhoto() (was reserving httpsRequestData but using httpsRequestSend)
  • Added explicit memory cleanup: release base64 photo buffer after upload to prevent heap fragmentation
  • Improved serial timeout configuration with explanatory comments
  • Enhanced sleep/idle messages with clearer descriptions of why WiFi is powering down
  • Fixed variable shadowing in buttonSaveNetworkCall() by renaming local variables
  • Added rounding in sendPhoto() to keep wire format clean (e.g., "25" instead of "25.70")

Notable Implementation Details

  • The serial timeout of 50ms bounds the maximum wait in the radar polling loop, preventing stalls if the STM32 doesn't respond
  • The buffer drain in get_speed() is critical for maintaining synchronization with the STM32's response stream
  • Float precision for speed values now matches the STM32's actual output resolution (0.1 units)

https://claude.ai/code/session_01AfFtNmP3SFYS9HwhkkhR54

claude and others added 11 commits May 8, 2026 18:17
- api.h: reserve() was called on the wrong String (httpsRequestData
  instead of httpsRequestSend), forcing many reallocations during the
  ~100KB photo upload payload build.
- api.h: release photo_base64 after upload so the buffer doesn't sit
  on the heap until the next capture.
- api.h: round maxSpeed -> speed_actual to keep the wire format integer
  now that maxSpeed is float.
- api.h: wifiResetButton() now stores "NOT_SET" sentinels (matches
  buttonClearNetworkCall) rather than literal "ssid"/"pass".
- radar.h: fix get_speed() race - the original checked available()
  before the STM32 could reply, so each cycle returned the previous
  query's data (or "NO DATA"). Drain stale bytes, send the command,
  then parseFloat() with a bounded Serial1 timeout.
- variables.h: speed and maxSpeed were int, truncating the radar's
  0.1 resolution; now float.
- esp32_firmware_1-2.ino: Serial1.setTimeout(50) so parseFloat() can't
  stall the polling loop for a full second.
- esp32_firmware_1-2.ino: post-boot grace was 10s despite the comment
  promising 120s; restore 120000.
- esp32_firmware_1-2.ino: log lines now reflect what actually happens
  (WiFi off, not light sleep, since esp_light_sleep_start is commented
  out).
- espui_settings.h: rename locals in buttonSaveNetworkCall so they no
  longer shadow the globals from variables.h.

https://claude.ai/code/session_01AfFtNmP3SFYS9HwhkkhR54
Dropped twelve unused globals from variables.h (API_photo,
photo_finished, sleep_idle_time, send_alert, lastTime, labelWifi,
display_wifi, button1, switchOne, status, apIP, DNS_PORT) and the
write-only send_API (along with its sole assignment in the main
sketch). The DNS_PORT/apIP pair was apparently intended for a
captive-portal dnsServer.start() that is never called; the
processNextRequest() loop is therefore a no-op, but that is left
for a separate fix.

https://claude.ai/code/session_01AfFtNmP3SFYS9HwhkkhR54
dnsServer.processNextRequest() was being called every loop iteration
but dnsServer.start() had never been invoked, so the captive portal
silently did nothing - users had to type 192.168.4.1 by hand.

connectWifiAP() now starts a wildcard DNS responder pointing at the
soft-AP IP whenever it falls into AP mode, so any DNS lookup from a
connected client lands on the ESPUI page (and most OSes detect this
as a captive portal and surface the configuration UI automatically).

The DNSServer global moved to variables.h so both the .ino (for
processNextRequest) and api.h (for start) can see it.

https://claude.ai/code/session_01AfFtNmP3SFYS9HwhkkhR54
Five high-impact items from the firmware review:

- AP-aware idle teardown. The grace-window and 5s-idle paths in
  taskCore1 unconditionally called WiFi.disconnect/WIFI_OFF, which
  killed the soft-AP while a user might still be configuring WiFi
  via the captive portal. Both paths now skip the teardown when
  WiFi.getMode() == WIFI_AP.
- Task watchdog. Both pinned tasks (Core 0 / Core 1) are now
  subscribed to the task WDT with a 30s timeout (covers the 5s
  HTTPS connect/read budgets and the 3s wifiResetButton blocking
  poll). The inner while(speed >= min_speed) tracking loop also
  heartbeats so a long pass can't trip the watchdog.
- WiFi password no longer logged to Serial in connectWifi.
- Soft-AP WPA2 + ESPUI basic auth. Both use a per-device password
  derived from the last 4 bytes of the WiFi MAC (8 hex chars,
  meets WPA2 minimum length), printed to Serial at boot. Closes
  the open-AP / unauthenticated-portal hole that let anyone in
  radio range overwrite stored credentials.
- takePhoto() now returns bool and clears photo_filename and
  photo_base64 on capture failure, and the caller in taskCore1
  only sets send_data / send_photo when a real frame was captured.
  Previously a failed capture mid-run could cause the next upload
  to re-send a stale image from a prior pass.

The non_speeding endpoint still never receives data (send_data is
only raised when photo_speed is reached, so under-threshold passes
are dropped) - that's a separate bug, left for a follow-up.

https://claude.ai/code/session_01AfFtNmP3SFYS9HwhkkhR54
send_data was only being raised inside the photo-capture branch, so
passes that crossed min_speed but never reached photo_speed - and
passes where takePhoto() failed - never triggered Core 0's upload.
The non_speeding endpoint was effectively dead.

Drop the inline send_data assignment and raise send_data once at the
end of the tracking run. send_photo (already gated on photo_captured)
still selects between the speeding and non_speeding endpoints, so
photo passes upload to /speeding_capture and the rest upload to
/non_speeding_capture with just the speed.

https://claude.ai/code/session_01AfFtNmP3SFYS9HwhkkhR54
issue_cdm324_reset() previously spun forever waiting for a newline
from the STM32. Because it runs from setup() before either pinned
task subscribes to the watchdog, a dead or unprogrammed STM32 wedged
the boot with no recovery. Add a 1000ms millis-based bound and log
the timeout instead of hanging.

Also wire up ArduinoOTA so the firmware can be updated without USB.
setupOTA() runs after sendLocalIP() and only enables OTA in STA
mode - the AP fallback is for first-time setup, not for OTA. The
auth password is the same MAC-derived per-device password used for
the soft-AP and ESPUI basic auth, so a LAN attacker can't push
arbitrary firmware. taskCore0 services ArduinoOTA.handle() each
loop iteration; the onProgress callback resets the task WDT so a
slow upload can't trip the 30s timeout.

OTA is reachable while WiFi is up - i.e., during the 120s post-boot
grace window and any active radar period. The mDNS service won't
re-register itself after the idle teardown, so reliable OTA in the
field means: reboot the device, then push within the grace window.

https://claude.ai/code/session_01AfFtNmP3SFYS9HwhkkhR54
The minispeedcam.com Bubble.io workflow token was hardcoded in api.h
(twice), so rotating it required reflashing every fielded device.

Token is now loaded from NVS in setup() with the original baked-in
value as the default fallback, so existing deployments keep working
without manual configuration. A new "API Token" text input on the
WiFi Settings tab lets the operator rotate it from the portal; on
Save we persist to NVS and reboot, picking up the new value via
sendLocalIP() and sendPhoto().

https://claude.ai/code/session_01AfFtNmP3SFYS9HwhkkhR54
The portal previously had no read-only surface, so debugging a
device in the field meant USB serial. The new Status tab (added as
the first tab so it's the landing page) shows live telemetry that
covers the most common questions:

- Current Speed - radar reading per polling cycle, in the configured
  unit (mph/kph).
- Last Run Max - peak speed of the most recent tracking pass.
- Last Upload - "OK (200)", "Failed (-1)", "Uploading...", or "No
  uploads yet". Reads httpsResponseCode and sending_data.
- WiFi - SSID and RSSI when STA-connected, "AP mode" during initial
  setup, "Radio off" when the idle path has powered the radio down.
- Uptime - h/m/s since boot.

updateStatusUI() lives in espui_settings.h; taskCore1 calls it on a
1Hz cadence (the radar loop runs at 10Hz, which is too fast for the
ESPUI WebSocket).

https://claude.ai/code/session_01AfFtNmP3SFYS9HwhkkhR54
esp_task_wdt_init's signature changed between Arduino-ESP32 2.x
(IDF 4: timeout_seconds + bool panic) and 3.x (IDF 5: const
esp_task_wdt_config_t*). The previous commit used the 2.x form,
which fails to compile on IDF 5.x toolchains:

    error: too many arguments to function
    'esp_err_t esp_task_wdt_init(const esp_task_wdt_config_t*)'

Switch to a #if ESP_IDF_VERSION_MAJOR >= 5 guard so both core
versions build. The 5.x branch fills out the config struct with
timeout_ms = 30000, idle_core_mask = 0 (we manually subscribe our
two pinned tasks), trigger_panic = true.

https://claude.ai/code/session_01AfFtNmP3SFYS9HwhkkhR54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants