feat: Add v0.3 OTA updates — dual partition, esp-ota tool, rollback
Dual OTA partition table (ota_0/ota_1, 1920 KB each) on 4MB flash. Firmware gains OTA command, LED_OTA double-blink, version in STATUS, and automatic rollback validation. Pi-side esp-ota tool serves firmware via HTTP and orchestrates the update flow. esp-fleet gains ota subcommand.
This commit is contained in:
+19
-13
@@ -8,25 +8,31 @@
|
|||||||
- [x] List firmware modification ideas with priorities
|
- [x] List firmware modification ideas with priorities
|
||||||
- [x] Verify build from repo (ESP-IDF v5.5.2, aarch64)
|
- [x] Verify build from repo (ESP-IDF v5.5.2, aarch64)
|
||||||
|
|
||||||
## v0.2 - Remote Management [IN PROGRESS]
|
## v0.2 - Remote Management [DONE]
|
||||||
- [x] Add UDP command listener on ESP32 (port 5501)
|
- [x] Add UDP command listener on ESP32 (port 5501)
|
||||||
- [x] Implement REBOOT command
|
- [x] Implement REBOOT command
|
||||||
- [x] Implement IDENTIFY command (LED solid 5s)
|
- [x] Implement IDENTIFY command (LED solid 5s)
|
||||||
- [x] Implement STATUS command (uptime, heap, RSSI, tx_power, rate)
|
- [x] Implement STATUS command (uptime, heap, RSSI, tx_power, rate, version)
|
||||||
- [x] Implement RATE command (change ping Hz, NVS persist)
|
- [x] Implement RATE command (change ping Hz, NVS persist)
|
||||||
- [x] Implement POWER command (TX power dBm, NVS persist)
|
- [x] Implement POWER command (TX power dBm, NVS persist)
|
||||||
- [x] Add LED status indicator (off/slow blink/fast blink/solid)
|
- [x] Add LED status indicator (off/slow blink/fast blink/solid)
|
||||||
- [x] NVS persistence for rate and tx_power settings
|
- [x] NVS persistence for rate and tx_power settings
|
||||||
- [x] Pi-side `esp-cmd` CLI tool
|
- [x] Pi-side `esp-cmd` and `esp-fleet` CLI tools
|
||||||
- [ ] Build and flash test on device
|
- [x] mDNS hostname, watchdog, human-readable uptime
|
||||||
- [ ] Update CHEATSHEET.md with new commands
|
- [x] Build and flash to device
|
||||||
|
- [x] Update CHEATSHEET.md with new commands
|
||||||
|
|
||||||
## v0.3 - OTA Updates
|
## v0.3 - OTA Updates [IN PROGRESS]
|
||||||
- [ ] Modify partition table for dual OTA
|
- [x] Dual OTA partition table (ota_0 + ota_1, 1920 KB each)
|
||||||
- [ ] Add OTA update handler in firmware
|
- [x] 4MB flash config, custom partitions in sdkconfig.defaults
|
||||||
- [ ] HTTP firmware server on Pi
|
- [x] OTA command handler + ota_task in firmware
|
||||||
- [ ] Pi-side flash script (select device, push firmware)
|
- [x] LED_OTA double-blink pattern during download
|
||||||
- [ ] Rollback on failed update
|
- [x] Bootloader rollback on failed update (30s watchdog)
|
||||||
|
- [x] Version field in STATUS reply
|
||||||
|
- [x] Pi-side `esp-ota` tool (HTTP server + OTA orchestration)
|
||||||
|
- [x] `esp-fleet ota` subcommand (sequential fleet update)
|
||||||
|
- [ ] USB-flash first device (partition table change)
|
||||||
|
- [ ] End-to-end OTA test
|
||||||
|
|
||||||
## v0.4 - Adaptive Sampling
|
## v0.4 - Adaptive Sampling
|
||||||
- [ ] On-device wander calculation (simplified)
|
- [ ] On-device wander calculation (simplified)
|
||||||
@@ -41,8 +47,8 @@
|
|||||||
- [ ] Pi-side BLE device tracking
|
- [ ] Pi-side BLE device tracking
|
||||||
|
|
||||||
## v1.0 - Production Firmware
|
## v1.0 - Production Firmware
|
||||||
- [ ] mDNS auto-discovery
|
- [x] mDNS auto-discovery (done in v0.2)
|
||||||
- [ ] Watchdog + auto-recovery
|
- [x] Watchdog + auto-recovery (done in v0.2)
|
||||||
- [ ] On-device CSI processing (send metrics, not raw)
|
- [ ] On-device CSI processing (send metrics, not raw)
|
||||||
- [ ] Configuration via UDP (WiFi SSID, target IP)
|
- [ ] Configuration via UDP (WiFi SSID, target IP)
|
||||||
- [ ] Comprehensive error handling
|
- [ ] Comprehensive error handling
|
||||||
|
|||||||
@@ -2,30 +2,45 @@
|
|||||||
|
|
||||||
**Last Updated:** 2026-02-04
|
**Last Updated:** 2026-02-04
|
||||||
|
|
||||||
## Current Sprint: v0.2 - Remote Management
|
## Current Sprint: v0.3 - OTA Updates
|
||||||
|
|
||||||
### P0 - Critical
|
### P0 - Critical
|
||||||
- [x] Firmware: UDP command listener (port 5501)
|
- [x] Create dual OTA partition table (`partitions.csv`)
|
||||||
- [x] Firmware: LED status indicator (GPIO2)
|
- [x] Update `sdkconfig.defaults` (4MB flash, custom partitions, rollback, HTTP OTA)
|
||||||
- [x] Firmware: NVS config persistence (rate, tx_power)
|
- [x] Firmware: OTA command, ota_task, LED_OTA, rollback validation
|
||||||
- [~] Build and flash firmware to device
|
- [x] Firmware: Add version to STATUS reply
|
||||||
|
- [ ] `idf.py reconfigure` to regenerate sdkconfig
|
||||||
|
- [ ] Build and USB-flash first device (partition table change requires USB)
|
||||||
|
|
||||||
### P1 - Important
|
### P1 - Important
|
||||||
- [x] Firmware: REBOOT, IDENTIFY, STATUS commands
|
- [x] Pi-side `esp-ota` tool (HTTP server + OTA orchestration)
|
||||||
- [x] Firmware: RATE command (10-100 Hz, restarts ping)
|
- [x] `esp-fleet ota` subcommand (sequential fleet OTA)
|
||||||
- [x] Firmware: POWER command (2-20 dBm)
|
- [ ] Test OTA end-to-end: `esp-ota amber-maple.local`
|
||||||
- [x] Firmware: Refactor ping to support restart
|
- [ ] Regenerate `sdkconfig.sample`
|
||||||
- [x] Pi-side: `esp-cmd` CLI tool
|
|
||||||
- [ ] Update CHEATSHEET.md with esp-cmd usage
|
|
||||||
|
|
||||||
### P2 - Normal
|
### P2 - Normal
|
||||||
- [ ] Document esp-cmd in USAGE.md
|
- [ ] OTA update remaining fleet (muddy-storm, hollow-acorn) via USB
|
||||||
- [ ] Add Kconfig CSI_CMD_PORT option
|
- [ ] Test rollback (flash bad firmware, verify auto-revert)
|
||||||
|
- [ ] Document esp-ota in USAGE.md
|
||||||
|
|
||||||
### P3 - Low
|
### P3 - Low
|
||||||
- [ ] Document esp-crab dual-antenna capabilities
|
- [ ] Document esp-crab dual-antenna capabilities
|
||||||
- [ ] Document esp-radar console features
|
- [ ] Document esp-radar console features
|
||||||
|
|
||||||
|
## Completed: v0.2 - Remote Management
|
||||||
|
|
||||||
|
- [x] Firmware: UDP command listener (port 5501)
|
||||||
|
- [x] Firmware: LED status indicator (GPIO2)
|
||||||
|
- [x] Firmware: NVS config persistence (rate, tx_power)
|
||||||
|
- [x] Firmware: REBOOT, IDENTIFY, STATUS commands
|
||||||
|
- [x] Firmware: RATE command (10-100 Hz, restarts ping)
|
||||||
|
- [x] Firmware: POWER command (2-20 dBm)
|
||||||
|
- [x] Pi-side: `esp-cmd` CLI tool
|
||||||
|
- [x] Pi-side: `esp-fleet` fleet management tool
|
||||||
|
- [x] mDNS hostname, watchdog, human-readable uptime
|
||||||
|
- [x] Build and flash to device
|
||||||
|
- [x] Update CHEATSHEET.md
|
||||||
|
|
||||||
## Completed: v0.1 - Documentation
|
## Completed: v0.1 - Documentation
|
||||||
|
|
||||||
- [x] Copy firmware sources to project
|
- [x] Copy firmware sources to project
|
||||||
@@ -34,12 +49,11 @@
|
|||||||
- [x] Create .gitignore for build artifacts
|
- [x] Create .gitignore for build artifacts
|
||||||
- [x] Test building firmware from this repo
|
- [x] Test building firmware from this repo
|
||||||
- [x] Document CSI config options
|
- [x] Document CSI config options
|
||||||
- [x] Compare csi_recv vs csi_recv_router differences
|
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Build confirmed working on ESP-IDF v5.5.2 (aarch64/Pi 5)
|
- NVS offset changes with new partition table — first USB flash resets saved config
|
||||||
- v0.2 firmware adds ~1.5 KB heap + 6 KB stack usage
|
- First device must be USB-flashed to switch partition table, subsequent updates via OTA
|
||||||
- NVS namespace: `csi_config` (keys: `send_rate`, `tx_power`)
|
- `esp_https_ota` is built into ESP-IDF core — no extra deps needed
|
||||||
- LED uses GPIO2 (built-in on most ESP32 dev boards)
|
- OTA download ~790 KB on LAN takes ~3-5s, well under 30s watchdog
|
||||||
- Command port default: 5501 (configurable via menuconfig)
|
- CSI data keeps flowing during OTA download
|
||||||
|
|||||||
+30
-1
@@ -34,10 +34,11 @@ idf.py reconfigure # Re-fetch managed components
|
|||||||
## Remote Management (esp-cmd)
|
## Remote Management (esp-cmd)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
esp-cmd <host> STATUS # Uptime, heap, RSSI, tx_power, rate
|
esp-cmd <host> STATUS # Uptime, heap, RSSI, tx_power, rate, version
|
||||||
esp-cmd <host> IDENTIFY # LED solid 5s (find the device)
|
esp-cmd <host> IDENTIFY # LED solid 5s (find the device)
|
||||||
esp-cmd <host> RATE 50 # Set ping rate to 50 Hz (NVS saved)
|
esp-cmd <host> RATE 50 # Set ping rate to 50 Hz (NVS saved)
|
||||||
esp-cmd <host> POWER 15 # Set TX power to 15 dBm (NVS saved)
|
esp-cmd <host> POWER 15 # Set TX power to 15 dBm (NVS saved)
|
||||||
|
esp-cmd <host> OTA http://pi:8070/fw # Trigger OTA update (use esp-ota instead)
|
||||||
esp-cmd <host> REBOOT # Restart device
|
esp-cmd <host> REBOOT # Restart device
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -55,8 +56,34 @@ esp-fleet status # Query all sensors at once
|
|||||||
esp-fleet identify # Blink all LEDs
|
esp-fleet identify # Blink all LEDs
|
||||||
esp-fleet rate 50 # Set rate on all devices
|
esp-fleet rate 50 # Set rate on all devices
|
||||||
esp-fleet reboot # Reboot entire fleet
|
esp-fleet reboot # Reboot entire fleet
|
||||||
|
esp-fleet ota # OTA update all (sequential)
|
||||||
|
esp-fleet ota /path/to/firmware.bin # OTA with custom firmware
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## OTA Updates (esp-ota)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
esp-ota amber-maple.local # OTA with default build
|
||||||
|
esp-ota amber-maple.local -f fw.bin # OTA with custom firmware
|
||||||
|
esp-ota amber-maple.local --no-wait # Fire and forget
|
||||||
|
```
|
||||||
|
|
||||||
|
**First flash after enabling OTA requires USB** (partition table change).
|
||||||
|
After that, all updates are OTA.
|
||||||
|
|
||||||
|
### OTA Flow
|
||||||
|
|
||||||
|
1. Verifies device is alive via STATUS
|
||||||
|
2. Starts temp HTTP server on Pi (port 8070)
|
||||||
|
3. Sends `OTA http://<pi>:8070/<fw>.bin` via UDP
|
||||||
|
4. Device downloads, flashes, reboots
|
||||||
|
5. Verifies device responds post-reboot
|
||||||
|
|
||||||
|
### Rollback
|
||||||
|
|
||||||
|
If new firmware crashes or hangs, the 30s watchdog reboots and bootloader
|
||||||
|
automatically rolls back to the previous firmware.
|
||||||
|
|
||||||
### LED States
|
### LED States
|
||||||
|
|
||||||
| LED | Meaning |
|
| LED | Meaning |
|
||||||
@@ -65,6 +92,7 @@ esp-fleet reboot # Reboot entire fleet
|
|||||||
| Slow blink (1 Hz) | Connected, no CSI activity |
|
| Slow blink (1 Hz) | Connected, no CSI activity |
|
||||||
| Fast blink (5 Hz) | CSI data flowing |
|
| Fast blink (5 Hz) | CSI data flowing |
|
||||||
| Solid (5s) | IDENTIFY command active |
|
| Solid (5s) | IDENTIFY command active |
|
||||||
|
| Double blink | OTA in progress |
|
||||||
|
|
||||||
## Test CSI Reception
|
## Test CSI Reception
|
||||||
|
|
||||||
@@ -109,6 +137,7 @@ secondary_channel,timestamp,ant,sig_len,rx_state,len,first_word,"[I,Q,...]"
|
|||||||
| `get-started/csi_recv_router/main/app_main.c` | Main firmware source |
|
| `get-started/csi_recv_router/main/app_main.c` | Main firmware source |
|
||||||
| `get-started/csi_recv_router/main/Kconfig.projbuild` | UDP/cmd port config |
|
| `get-started/csi_recv_router/main/Kconfig.projbuild` | UDP/cmd port config |
|
||||||
| `tools/esp-cmd` | Pi-side management CLI |
|
| `tools/esp-cmd` | Pi-side management CLI |
|
||||||
|
| `tools/esp-ota` | Pi-side OTA update tool |
|
||||||
| `tools/esp-fleet` | Fleet-wide command tool |
|
| `tools/esp-fleet` | Fleet-wide command tool |
|
||||||
| `get-started/csi_recv_router/sdkconfig.defaults` | SDK defaults |
|
| `get-started/csi_recv_router/sdkconfig.defaults` | SDK defaults |
|
||||||
| `get-started/csi_recv_router/main/idf_component.yml` | Dependencies |
|
| `get-started/csi_recv_router/main/idf_component.yml` | Dependencies |
|
||||||
|
|||||||
@@ -30,6 +30,9 @@
|
|||||||
#include "esp_now.h"
|
#include "esp_now.h"
|
||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
#include "esp_task_wdt.h"
|
#include "esp_task_wdt.h"
|
||||||
|
#include "esp_ota_ops.h"
|
||||||
|
#include "esp_https_ota.h"
|
||||||
|
#include "esp_http_client.h"
|
||||||
#include "driver/gpio.h"
|
#include "driver/gpio.h"
|
||||||
#include "mdns.h"
|
#include "mdns.h"
|
||||||
|
|
||||||
@@ -65,6 +68,7 @@ typedef enum {
|
|||||||
LED_SLOW_BLINK,
|
LED_SLOW_BLINK,
|
||||||
LED_FAST_BLINK,
|
LED_FAST_BLINK,
|
||||||
LED_SOLID,
|
LED_SOLID,
|
||||||
|
LED_OTA,
|
||||||
} led_mode_t;
|
} led_mode_t;
|
||||||
|
|
||||||
/* --- Globals --- */
|
/* --- Globals --- */
|
||||||
@@ -74,6 +78,7 @@ static esp_ping_handle_t s_ping_handle = NULL;
|
|||||||
static volatile led_mode_t s_led_mode = LED_OFF;
|
static volatile led_mode_t s_led_mode = LED_OFF;
|
||||||
static volatile int64_t s_last_csi_time = 0;
|
static volatile int64_t s_last_csi_time = 0;
|
||||||
static volatile int64_t s_identify_end_time = 0;
|
static volatile int64_t s_identify_end_time = 0;
|
||||||
|
static volatile bool s_ota_in_progress = false;
|
||||||
|
|
||||||
/* UDP socket for CSI data transmission */
|
/* UDP socket for CSI data transmission */
|
||||||
static int s_udp_socket = -1;
|
static int s_udp_socket = -1;
|
||||||
@@ -181,6 +186,18 @@ static void led_task(void *arg)
|
|||||||
led_on = true;
|
led_on = true;
|
||||||
vTaskDelay(pdMS_TO_TICKS(200));
|
vTaskDelay(pdMS_TO_TICKS(200));
|
||||||
break;
|
break;
|
||||||
|
case LED_OTA:
|
||||||
|
/* Double-blink: on-off-on-off-pause */
|
||||||
|
gpio_set_level(LED_GPIO, 1);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(80));
|
||||||
|
gpio_set_level(LED_GPIO, 0);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(80));
|
||||||
|
gpio_set_level(LED_GPIO, 1);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(80));
|
||||||
|
gpio_set_level(LED_GPIO, 0);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
led_on = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,6 +392,41 @@ static esp_err_t wifi_ping_router_start(void)
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- OTA --- */
|
||||||
|
|
||||||
|
static void ota_task(void *arg)
|
||||||
|
{
|
||||||
|
char *url = (char *)arg;
|
||||||
|
ESP_LOGI(TAG, "OTA: downloading from %s", url);
|
||||||
|
|
||||||
|
s_led_mode = LED_OTA;
|
||||||
|
|
||||||
|
esp_http_client_config_t http_cfg = {
|
||||||
|
.url = url,
|
||||||
|
.timeout_ms = 30000,
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_https_ota_config_t ota_cfg = {
|
||||||
|
.http_config = &http_cfg,
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_err_t err = esp_https_ota(&ota_cfg);
|
||||||
|
free(url);
|
||||||
|
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "OTA: success, rebooting...");
|
||||||
|
s_led_mode = LED_SOLID;
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
esp_restart();
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(TAG, "OTA: failed: %s", esp_err_to_name(err));
|
||||||
|
s_led_mode = LED_SLOW_BLINK;
|
||||||
|
s_ota_in_progress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Command handler --- */
|
/* --- Command handler --- */
|
||||||
|
|
||||||
static void reboot_after_delay(void *arg)
|
static void reboot_after_delay(void *arg)
|
||||||
@@ -414,6 +466,8 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size)
|
|||||||
rssi = ap.rssi;
|
rssi = ap.rssi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const esp_app_desc_t *app_desc = esp_app_get_description();
|
||||||
|
|
||||||
char uptime_str[32];
|
char uptime_str[32];
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
snprintf(uptime_str, sizeof(uptime_str), "%dd%dh%dm", days, hours, mins);
|
snprintf(uptime_str, sizeof(uptime_str), "%dd%dh%dm", days, hours, mins);
|
||||||
@@ -424,9 +478,9 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size)
|
|||||||
}
|
}
|
||||||
|
|
||||||
snprintf(reply, reply_size,
|
snprintf(reply, reply_size,
|
||||||
"OK STATUS uptime=%s heap=%lu rssi=%d tx_power=%d rate=%d hostname=%s",
|
"OK STATUS uptime=%s heap=%lu rssi=%d tx_power=%d rate=%d hostname=%s version=%s",
|
||||||
uptime_str, (unsigned long)heap, rssi, (int)s_tx_power_dbm,
|
uptime_str, (unsigned long)heap, rssi, (int)s_tx_power_dbm,
|
||||||
s_send_frequency, CONFIG_CSI_HOSTNAME);
|
s_send_frequency, CONFIG_CSI_HOSTNAME, app_desc->version);
|
||||||
return strlen(reply);
|
return strlen(reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,6 +512,28 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size)
|
|||||||
return strlen(reply);
|
return strlen(reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* OTA <url> */
|
||||||
|
if (strncmp(cmd, "OTA ", 4) == 0) {
|
||||||
|
const char *url = cmd + 4;
|
||||||
|
if (strncmp(url, "http://", 7) != 0) {
|
||||||
|
snprintf(reply, reply_size, "ERR OTA url must start with http://");
|
||||||
|
return strlen(reply);
|
||||||
|
}
|
||||||
|
if (s_ota_in_progress) {
|
||||||
|
snprintf(reply, reply_size, "ERR OTA already in progress");
|
||||||
|
return strlen(reply);
|
||||||
|
}
|
||||||
|
char *url_copy = strdup(url);
|
||||||
|
if (!url_copy) {
|
||||||
|
snprintf(reply, reply_size, "ERR OTA out of memory");
|
||||||
|
return strlen(reply);
|
||||||
|
}
|
||||||
|
s_ota_in_progress = true;
|
||||||
|
xTaskCreate(ota_task, "ota_task", 8192, url_copy, 5, NULL);
|
||||||
|
snprintf(reply, reply_size, "OK OTA started");
|
||||||
|
return strlen(reply);
|
||||||
|
}
|
||||||
|
|
||||||
snprintf(reply, reply_size, "ERR UNKNOWN");
|
snprintf(reply, reply_size, "ERR UNKNOWN");
|
||||||
return strlen(reply);
|
return strlen(reply);
|
||||||
}
|
}
|
||||||
@@ -563,4 +639,14 @@ void app_main()
|
|||||||
wifi_ping_router_start();
|
wifi_ping_router_start();
|
||||||
|
|
||||||
xTaskCreate(cmd_task, "cmd_task", 4096, NULL, 5, NULL);
|
xTaskCreate(cmd_task, "cmd_task", 4096, NULL, 5, NULL);
|
||||||
|
|
||||||
|
/* OTA rollback: mark firmware valid if we got this far */
|
||||||
|
const esp_partition_t *running = esp_ota_get_running_partition();
|
||||||
|
esp_ota_img_states_t ota_state;
|
||||||
|
if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {
|
||||||
|
if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {
|
||||||
|
ESP_LOGI(TAG, "OTA: marking firmware valid (rollback cancelled)");
|
||||||
|
esp_ota_mark_app_valid_cancel_rollback();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Name, Type, SubType, Offset, Size
|
||||||
|
nvs, data, nvs, 0x9000, 0x4000
|
||||||
|
otadata, data, ota, 0xd000, 0x2000
|
||||||
|
phy_init, data, phy, 0xf000, 0x1000
|
||||||
|
ota_0, app, ota_0, 0x10000, 0x1E0000
|
||||||
|
ota_1, app, ota_1, 0x1F0000, 0x1E0000
|
||||||
|
@@ -49,3 +49,16 @@ CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32
|
|||||||
#
|
#
|
||||||
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
|
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
|
||||||
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=240
|
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=240
|
||||||
|
|
||||||
|
#
|
||||||
|
# Flash & Partitions (4MB flash, dual OTA)
|
||||||
|
#
|
||||||
|
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||||
|
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||||
|
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
||||||
|
|
||||||
|
#
|
||||||
|
# OTA Updates
|
||||||
|
#
|
||||||
|
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
|
||||||
|
CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y
|
||||||
|
|||||||
+30
-1
@@ -2,7 +2,9 @@
|
|||||||
"""Query all ESP32 CSI sensors in parallel."""
|
"""Query all ESP32 CSI sensors in parallel."""
|
||||||
|
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
|
import os
|
||||||
import socket
|
import socket
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
DEFAULT_PORT = 5501
|
DEFAULT_PORT = 5501
|
||||||
@@ -14,6 +16,8 @@ SENSORS = [
|
|||||||
("hollow-acorn", "hollow-acorn.local"),
|
("hollow-acorn", "hollow-acorn.local"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
ESP_OTA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "esp-ota")
|
||||||
|
|
||||||
USAGE = """\
|
USAGE = """\
|
||||||
Usage: esp-fleet <command> [args...]
|
Usage: esp-fleet <command> [args...]
|
||||||
|
|
||||||
@@ -25,11 +29,14 @@ Commands:
|
|||||||
rate <10-100> Set ping rate on all devices
|
rate <10-100> Set ping rate on all devices
|
||||||
power <2-20> Set TX power on all devices
|
power <2-20> Set TX power on all devices
|
||||||
reboot Reboot all devices
|
reboot Reboot all devices
|
||||||
|
ota [firmware.bin] OTA update all devices (sequentially)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
esp-fleet status
|
esp-fleet status
|
||||||
esp-fleet identify
|
esp-fleet identify
|
||||||
esp-fleet rate 50"""
|
esp-fleet rate 50
|
||||||
|
esp-fleet ota
|
||||||
|
esp-fleet ota /path/to/firmware.bin"""
|
||||||
|
|
||||||
|
|
||||||
def query(name, host, cmd):
|
def query(name, host, cmd):
|
||||||
@@ -54,11 +61,33 @@ def query(name, host, cmd):
|
|||||||
sock.close()
|
sock.close()
|
||||||
|
|
||||||
|
|
||||||
|
def run_ota(firmware=None):
|
||||||
|
"""Run OTA on each sensor sequentially."""
|
||||||
|
for name, host in SENSORS:
|
||||||
|
print(f"\n{'='*40}")
|
||||||
|
print(f"OTA: {name} ({host})")
|
||||||
|
print(f"{'='*40}")
|
||||||
|
cmd = [ESP_OTA, host]
|
||||||
|
if firmware:
|
||||||
|
cmd += ["-f", firmware]
|
||||||
|
result = subprocess.run(cmd)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"ERR: OTA failed for {name}, stopping fleet OTA", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
print(f"\nAll {len(SENSORS)} devices updated.")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
|
if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
|
||||||
print(USAGE)
|
print(USAGE)
|
||||||
sys.exit(0 if sys.argv[1:] and sys.argv[1] in ("-h", "--help") else 2)
|
sys.exit(0 if sys.argv[1:] and sys.argv[1] in ("-h", "--help") else 2)
|
||||||
|
|
||||||
|
# Handle OTA subcommand separately (sequential, not parallel)
|
||||||
|
if sys.argv[1].lower() == "ota":
|
||||||
|
firmware = sys.argv[2] if len(sys.argv) > 2 else None
|
||||||
|
run_ota(firmware)
|
||||||
|
return
|
||||||
|
|
||||||
cmd = " ".join(sys.argv[1:]).strip().upper()
|
cmd = " ".join(sys.argv[1:]).strip().upper()
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=len(SENSORS)) as pool:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=len(SENSORS)) as pool:
|
||||||
|
|||||||
Executable
+149
@@ -0,0 +1,149 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""OTA firmware update for ESP32 CSI devices."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import http.server
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
DEFAULT_CMD_PORT = 5501
|
||||||
|
DEFAULT_HTTP_PORT = 8070
|
||||||
|
DEFAULT_FW = os.path.expanduser(
|
||||||
|
"~/git/esp32-hacking/get-started/csi_recv_router/build/csi_recv_router.bin"
|
||||||
|
)
|
||||||
|
TIMEOUT = 2.0
|
||||||
|
REBOOT_WAIT = 25.0
|
||||||
|
STATUS_RETRIES = 10
|
||||||
|
STATUS_INTERVAL = 3.0
|
||||||
|
|
||||||
|
|
||||||
|
def resolve(host: str) -> str:
|
||||||
|
"""Resolve hostname to IP address."""
|
||||||
|
try:
|
||||||
|
result = socket.getaddrinfo(host, DEFAULT_CMD_PORT, socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
return result[0][4][0]
|
||||||
|
except socket.gaierror as e:
|
||||||
|
print(f"ERR: cannot resolve {host}: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def udp_cmd(ip: str, cmd: str, timeout: float = TIMEOUT) -> str:
|
||||||
|
"""Send UDP command and return reply."""
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
try:
|
||||||
|
sock.sendto(cmd.encode(), (ip, DEFAULT_CMD_PORT))
|
||||||
|
data, _ = sock.recvfrom(512)
|
||||||
|
return data.decode().strip()
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_local_ip(target_ip: str) -> str:
|
||||||
|
"""Determine which local IP the target can reach us on."""
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
try:
|
||||||
|
sock.connect((target_ip, 80))
|
||||||
|
return sock.getsockname()[0]
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
|
||||||
|
def serve_firmware(directory: str, port: int) -> http.server.HTTPServer:
|
||||||
|
"""Start HTTP server serving firmware directory in a background thread."""
|
||||||
|
handler = lambda *a, **k: http.server.SimpleHTTPRequestHandler(
|
||||||
|
*a, directory=directory, **k
|
||||||
|
)
|
||||||
|
server = http.server.HTTPServer(("0.0.0.0", port), handler)
|
||||||
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
return server
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="OTA firmware update for ESP32 CSI devices")
|
||||||
|
parser.add_argument("host", help="Device hostname or IP (e.g., amber-maple.local)")
|
||||||
|
parser.add_argument("-f", "--firmware", default=DEFAULT_FW, help="Path to firmware .bin")
|
||||||
|
parser.add_argument("-p", "--port", type=int, default=DEFAULT_HTTP_PORT, help="HTTP server port")
|
||||||
|
parser.add_argument("--no-wait", action="store_true", help="Don't wait for reboot verification")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
fw_path = os.path.abspath(args.firmware)
|
||||||
|
if not os.path.isfile(fw_path):
|
||||||
|
print(f"ERR: firmware not found: {fw_path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
fw_size = os.path.getsize(fw_path)
|
||||||
|
fw_dir = os.path.dirname(fw_path)
|
||||||
|
fw_name = os.path.basename(fw_path)
|
||||||
|
|
||||||
|
print(f"Firmware: {fw_path} ({fw_size // 1024} KB)")
|
||||||
|
|
||||||
|
# Resolve device
|
||||||
|
ip = resolve(args.host)
|
||||||
|
print(f"Device: {args.host} ({ip})")
|
||||||
|
|
||||||
|
# Check device is alive
|
||||||
|
try:
|
||||||
|
reply = udp_cmd(ip, "STATUS")
|
||||||
|
print(f"Status: {reply}")
|
||||||
|
except (socket.timeout, OSError) as e:
|
||||||
|
print(f"ERR: device not responding: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Determine local IP
|
||||||
|
local_ip = get_local_ip(ip)
|
||||||
|
ota_url = f"http://{local_ip}:{args.port}/{fw_name}"
|
||||||
|
print(f"OTA URL: {ota_url}")
|
||||||
|
|
||||||
|
# Start HTTP server
|
||||||
|
server = serve_firmware(fw_dir, args.port)
|
||||||
|
print(f"HTTP server on port {args.port}")
|
||||||
|
|
||||||
|
# Send OTA command
|
||||||
|
try:
|
||||||
|
reply = udp_cmd(ip, f"OTA {ota_url}")
|
||||||
|
print(f"OTA cmd: {reply}")
|
||||||
|
except (socket.timeout, OSError) as e:
|
||||||
|
server.shutdown()
|
||||||
|
print(f"ERR: OTA command failed: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not reply.startswith("OK"):
|
||||||
|
server.shutdown()
|
||||||
|
print(f"ERR: device rejected OTA: {reply}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if args.no_wait:
|
||||||
|
print("OTA started (--no-wait, not verifying)")
|
||||||
|
# Keep server alive briefly for download
|
||||||
|
time.sleep(30)
|
||||||
|
server.shutdown()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Wait for device to download, flash, and reboot
|
||||||
|
print(f"Waiting for reboot...")
|
||||||
|
time.sleep(REBOOT_WAIT)
|
||||||
|
|
||||||
|
# Verify device comes back
|
||||||
|
for attempt in range(1, STATUS_RETRIES + 1):
|
||||||
|
try:
|
||||||
|
ip = resolve(args.host)
|
||||||
|
reply = udp_cmd(ip, "STATUS", timeout=3.0)
|
||||||
|
print(f"Verified: {reply}")
|
||||||
|
server.shutdown()
|
||||||
|
return
|
||||||
|
except (socket.timeout, OSError):
|
||||||
|
print(f" retry {attempt}/{STATUS_RETRIES}...")
|
||||||
|
time.sleep(STATUS_INTERVAL)
|
||||||
|
|
||||||
|
server.shutdown()
|
||||||
|
print("ERR: device did not come back after OTA", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user