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:
user
2026-02-04 16:19:09 +01:00
parent 44bd549761
commit d65ac208b9
8 changed files with 368 additions and 36 deletions

View File

@@ -30,6 +30,9 @@
#include "esp_now.h"
#include "esp_timer.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 "mdns.h"
@@ -65,6 +68,7 @@ typedef enum {
LED_SLOW_BLINK,
LED_FAST_BLINK,
LED_SOLID,
LED_OTA,
} led_mode_t;
/* --- 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 int64_t s_last_csi_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 */
static int s_udp_socket = -1;
@@ -181,6 +186,18 @@ static void led_task(void *arg)
led_on = true;
vTaskDelay(pdMS_TO_TICKS(200));
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;
}
/* --- 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 --- */
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;
}
const esp_app_desc_t *app_desc = esp_app_get_description();
char uptime_str[32];
if (days > 0) {
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,
"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,
s_send_frequency, CONFIG_CSI_HOSTNAME);
s_send_frequency, CONFIG_CSI_HOSTNAME, app_desc->version);
return strlen(reply);
}
@@ -458,6 +512,28 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size)
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");
return strlen(reply);
}
@@ -563,4 +639,14 @@ void app_main()
wifi_ping_router_start();
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();
}
}
}

View File

@@ -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
1 # Name Type SubType Offset Size
2 nvs data nvs 0x9000 0x4000
3 otadata data ota 0xd000 0x2000
4 phy_init data phy 0xf000 0x1000
5 ota_0 app ota_0 0x10000 0x1E0000
6 ota_1 app ota_1 0x1F0000 0x1E0000

View File

@@ -49,3 +49,16 @@ CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32
#
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
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