Merge pull request 'feat: ESP32-S3 OTA stack — partitions, Gitea checker, self-update, UART IO, display, Orin serial trigger (6 beads)' (#731) from sl-firmware/ota-esp32 into main
This commit is contained in:
commit
329797d43c
@ -1,4 +1,22 @@
|
|||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS "main.c" "orin_serial.c" "vesc_can.c"
|
SRCS
|
||||||
|
"main.c"
|
||||||
|
"orin_serial.c"
|
||||||
|
"vesc_can.c"
|
||||||
|
"gitea_ota.c"
|
||||||
|
"ota_self.c"
|
||||||
|
"uart_ota.c"
|
||||||
|
"ota_display.c"
|
||||||
INCLUDE_DIRS "."
|
INCLUDE_DIRS "."
|
||||||
|
REQUIRES
|
||||||
|
esp_wifi
|
||||||
|
esp_http_client
|
||||||
|
esp_https_ota
|
||||||
|
nvs_flash
|
||||||
|
app_update
|
||||||
|
mbedtls
|
||||||
|
cJSON
|
||||||
|
driver
|
||||||
|
freertos
|
||||||
|
esp_timer
|
||||||
)
|
)
|
||||||
|
|||||||
285
esp32s3/balance/main/gitea_ota.c
Normal file
285
esp32s3/balance/main/gitea_ota.c
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
/* gitea_ota.c — Gitea version checker (bd-3hte)
|
||||||
|
*
|
||||||
|
* Uses esp_http_client + cJSON to query:
|
||||||
|
* GET /api/v1/repos/{repo}/releases?limit=10
|
||||||
|
* Filters releases by tag prefix, extracts version and download URLs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "gitea_ota.h"
|
||||||
|
#include "version.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_wifi.h"
|
||||||
|
#include "esp_event.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
#include "esp_http_client.h"
|
||||||
|
#include "nvs_flash.h"
|
||||||
|
#include "nvs.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "freertos/event_groups.h"
|
||||||
|
#include "cJSON.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
static const char *TAG = "gitea_ota";
|
||||||
|
|
||||||
|
ota_update_info_t g_balance_update = {0};
|
||||||
|
ota_update_info_t g_io_update = {0};
|
||||||
|
|
||||||
|
/* ── WiFi connection ── */
|
||||||
|
#define WIFI_CONNECTED_BIT BIT0
|
||||||
|
#define WIFI_FAIL_BIT BIT1
|
||||||
|
#define WIFI_MAX_RETRIES 5
|
||||||
|
|
||||||
|
/* Compile-time WiFi fallback (override in NVS "wifi"/"ssid","pass") */
|
||||||
|
#define DEFAULT_WIFI_SSID "saltylab"
|
||||||
|
#define DEFAULT_WIFI_PASS ""
|
||||||
|
|
||||||
|
static EventGroupHandle_t s_wifi_eg;
|
||||||
|
static int s_wifi_retries = 0;
|
||||||
|
|
||||||
|
static void wifi_event_handler(void *arg, esp_event_base_t base,
|
||||||
|
int32_t id, void *data)
|
||||||
|
{
|
||||||
|
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
|
||||||
|
esp_wifi_connect();
|
||||||
|
} else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||||
|
if (s_wifi_retries < WIFI_MAX_RETRIES) {
|
||||||
|
esp_wifi_connect();
|
||||||
|
s_wifi_retries++;
|
||||||
|
} else {
|
||||||
|
xEventGroupSetBits(s_wifi_eg, WIFI_FAIL_BIT);
|
||||||
|
}
|
||||||
|
} else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
|
||||||
|
s_wifi_retries = 0;
|
||||||
|
xEventGroupSetBits(s_wifi_eg, WIFI_CONNECTED_BIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool wifi_connect(void)
|
||||||
|
{
|
||||||
|
char ssid[64] = DEFAULT_WIFI_SSID;
|
||||||
|
char pass[64] = DEFAULT_WIFI_PASS;
|
||||||
|
|
||||||
|
/* Try to read credentials from NVS */
|
||||||
|
nvs_handle_t nvs;
|
||||||
|
if (nvs_open("wifi", NVS_READONLY, &nvs) == ESP_OK) {
|
||||||
|
size_t sz = sizeof(ssid);
|
||||||
|
nvs_get_str(nvs, "ssid", ssid, &sz);
|
||||||
|
sz = sizeof(pass);
|
||||||
|
nvs_get_str(nvs, "pass", pass, &sz);
|
||||||
|
nvs_close(nvs);
|
||||||
|
}
|
||||||
|
|
||||||
|
s_wifi_eg = xEventGroupCreate();
|
||||||
|
s_wifi_retries = 0;
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(esp_netif_init());
|
||||||
|
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||||
|
esp_netif_create_default_wifi_sta();
|
||||||
|
|
||||||
|
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||||
|
|
||||||
|
esp_event_handler_instance_t h1, h2;
|
||||||
|
ESP_ERROR_CHECK(esp_event_handler_instance_register(
|
||||||
|
WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL, &h1));
|
||||||
|
ESP_ERROR_CHECK(esp_event_handler_instance_register(
|
||||||
|
IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL, &h2));
|
||||||
|
|
||||||
|
wifi_config_t wcfg = {0};
|
||||||
|
strlcpy((char *)wcfg.sta.ssid, ssid, sizeof(wcfg.sta.ssid));
|
||||||
|
strlcpy((char *)wcfg.sta.password, pass, sizeof(wcfg.sta.password));
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wcfg));
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_start());
|
||||||
|
|
||||||
|
EventBits_t bits = xEventGroupWaitBits(s_wifi_eg,
|
||||||
|
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE,
|
||||||
|
pdMS_TO_TICKS(15000));
|
||||||
|
|
||||||
|
esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, h2);
|
||||||
|
esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, h1);
|
||||||
|
vEventGroupDelete(s_wifi_eg);
|
||||||
|
|
||||||
|
if (bits & WIFI_CONNECTED_BIT) {
|
||||||
|
ESP_LOGI(TAG, "WiFi connected SSID=%s", ssid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
ESP_LOGW(TAG, "WiFi connect failed SSID=%s", ssid);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── HTTP fetch into a heap buffer ── */
|
||||||
|
#define HTTP_RESP_MAX (8 * 1024)
|
||||||
|
|
||||||
|
typedef struct { char *buf; int len; int cap; } http_buf_t;
|
||||||
|
|
||||||
|
static esp_err_t http_event_cb(esp_http_client_event_t *evt)
|
||||||
|
{
|
||||||
|
http_buf_t *b = (http_buf_t *)evt->user_data;
|
||||||
|
if (evt->event_id == HTTP_EVENT_ON_DATA && b) {
|
||||||
|
if (b->len + evt->data_len < b->cap) {
|
||||||
|
memcpy(b->buf + b->len, evt->data, evt->data_len);
|
||||||
|
b->len += evt->data_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *http_get(const char *url)
|
||||||
|
{
|
||||||
|
char *buf = malloc(HTTP_RESP_MAX);
|
||||||
|
if (!buf) return NULL;
|
||||||
|
http_buf_t b = {.buf = buf, .len = 0, .cap = HTTP_RESP_MAX};
|
||||||
|
buf[0] = '\0';
|
||||||
|
|
||||||
|
esp_http_client_config_t cfg = {
|
||||||
|
.url = url,
|
||||||
|
.event_handler = http_event_cb,
|
||||||
|
.user_data = &b,
|
||||||
|
.timeout_ms = GITEA_API_TIMEOUT_MS,
|
||||||
|
.skip_cert_common_name_check = true,
|
||||||
|
};
|
||||||
|
esp_http_client_handle_t client = esp_http_client_init(&cfg);
|
||||||
|
esp_err_t err = esp_http_client_perform(client);
|
||||||
|
int status = esp_http_client_get_status_code(client);
|
||||||
|
esp_http_client_cleanup(client);
|
||||||
|
|
||||||
|
if (err != ESP_OK || status != 200) {
|
||||||
|
ESP_LOGW(TAG, "HTTP GET %s → err=%d status=%d", url, err, status);
|
||||||
|
free(buf);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
buf[b.len] = '\0';
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Version comparison: returns true if remote > local ── */
|
||||||
|
static bool version_newer(const char *local, const char *remote)
|
||||||
|
{
|
||||||
|
int la=0,lb=0,lc=0, ra=0,rb=0,rc=0;
|
||||||
|
sscanf(local, "%d.%d.%d", &la, &lb, &lc);
|
||||||
|
sscanf(remote, "%d.%d.%d", &ra, &rb, &rc);
|
||||||
|
if (ra != la) return ra > la;
|
||||||
|
if (rb != lb) return rb > lb;
|
||||||
|
return rc > lc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Parse releases JSON array, fill ota_update_info_t ── */
|
||||||
|
static void parse_releases(const char *json, const char *tag_prefix,
|
||||||
|
const char *bin_asset, const char *sha_asset,
|
||||||
|
const char *local_version,
|
||||||
|
ota_update_info_t *out)
|
||||||
|
{
|
||||||
|
cJSON *arr = cJSON_Parse(json);
|
||||||
|
if (!arr || !cJSON_IsArray(arr)) {
|
||||||
|
ESP_LOGW(TAG, "JSON parse failed");
|
||||||
|
cJSON_Delete(arr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *rel;
|
||||||
|
cJSON_ArrayForEach(rel, arr) {
|
||||||
|
cJSON *tag_j = cJSON_GetObjectItem(rel, "tag_name");
|
||||||
|
if (!cJSON_IsString(tag_j)) continue;
|
||||||
|
|
||||||
|
const char *tag = tag_j->valuestring;
|
||||||
|
if (strncmp(tag, tag_prefix, strlen(tag_prefix)) != 0) continue;
|
||||||
|
|
||||||
|
/* Extract version after prefix */
|
||||||
|
const char *ver = tag + strlen(tag_prefix);
|
||||||
|
if (*ver == 'v') ver++; /* strip leading 'v' */
|
||||||
|
|
||||||
|
if (!version_newer(local_version, ver)) continue;
|
||||||
|
|
||||||
|
/* Found a newer release — extract asset URLs */
|
||||||
|
cJSON *assets = cJSON_GetObjectItem(rel, "assets");
|
||||||
|
if (!cJSON_IsArray(assets)) continue;
|
||||||
|
|
||||||
|
out->available = false;
|
||||||
|
out->download_url[0] = '\0';
|
||||||
|
out->sha256[0] = '\0';
|
||||||
|
strlcpy(out->version, ver, sizeof(out->version));
|
||||||
|
|
||||||
|
cJSON *asset;
|
||||||
|
cJSON_ArrayForEach(asset, assets) {
|
||||||
|
cJSON *name_j = cJSON_GetObjectItem(asset, "name");
|
||||||
|
cJSON *url_j = cJSON_GetObjectItem(asset, "browser_download_url");
|
||||||
|
if (!cJSON_IsString(name_j) || !cJSON_IsString(url_j)) continue;
|
||||||
|
|
||||||
|
if (strcmp(name_j->valuestring, bin_asset) == 0) {
|
||||||
|
strlcpy(out->download_url, url_j->valuestring,
|
||||||
|
sizeof(out->download_url));
|
||||||
|
out->available = true;
|
||||||
|
} else if (strcmp(name_j->valuestring, sha_asset) == 0) {
|
||||||
|
/* Download the SHA256 asset inline */
|
||||||
|
char *sha = http_get(url_j->valuestring);
|
||||||
|
if (sha) {
|
||||||
|
/* sha file is just hex+newline */
|
||||||
|
size_t n = strspn(sha, "0123456789abcdefABCDEF");
|
||||||
|
if (n == 64) {
|
||||||
|
memcpy(out->sha256, sha, 64);
|
||||||
|
out->sha256[64] = '\0';
|
||||||
|
}
|
||||||
|
free(sha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out->available) {
|
||||||
|
ESP_LOGI(TAG, "update: tag=%s ver=%s", tag, out->version);
|
||||||
|
}
|
||||||
|
break; /* use first matching release */
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_Delete(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Main check ── */
|
||||||
|
void gitea_ota_check_now(void)
|
||||||
|
{
|
||||||
|
char url[512];
|
||||||
|
snprintf(url, sizeof(url),
|
||||||
|
"%s/api/v1/repos/%s/releases?limit=10",
|
||||||
|
GITEA_BASE_URL, GITEA_REPO);
|
||||||
|
|
||||||
|
char *json = http_get(url);
|
||||||
|
if (!json) {
|
||||||
|
ESP_LOGW(TAG, "releases fetch failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_releases(json, BALANCE_TAG_PREFIX, BALANCE_BIN_ASSET,
|
||||||
|
BALANCE_SHA256_ASSET, BALANCE_FW_VERSION, &g_balance_update);
|
||||||
|
parse_releases(json, IO_TAG_PREFIX, IO_BIN_ASSET,
|
||||||
|
IO_SHA256_ASSET, IO_FW_VERSION, &g_io_update);
|
||||||
|
free(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Background task ── */
|
||||||
|
static void version_check_task(void *arg)
|
||||||
|
{
|
||||||
|
/* Initial check immediately after WiFi up */
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||||
|
gitea_ota_check_now();
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(VERSION_CHECK_PERIOD_MS));
|
||||||
|
gitea_ota_check_now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void gitea_ota_init(void)
|
||||||
|
{
|
||||||
|
ESP_ERROR_CHECK(nvs_flash_init());
|
||||||
|
|
||||||
|
if (!wifi_connect()) {
|
||||||
|
ESP_LOGW(TAG, "WiFi unavailable — version checks disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xTaskCreate(version_check_task, "ver_check", 6144, NULL, 3, NULL);
|
||||||
|
ESP_LOGI(TAG, "version check task started");
|
||||||
|
}
|
||||||
42
esp32s3/balance/main/gitea_ota.h
Normal file
42
esp32s3/balance/main/gitea_ota.h
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
/* gitea_ota.h — Gitea release version checker (bd-3hte)
|
||||||
|
*
|
||||||
|
* WiFi task: on boot and every 30 min, queries Gitea releases API,
|
||||||
|
* compares tag version against embedded FW_VERSION, stores update info.
|
||||||
|
*
|
||||||
|
* WiFi credentials read from NVS namespace "wifi" keys "ssid"/"pass".
|
||||||
|
* Fall back to compile-time defaults if NVS is empty.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/* Gitea instance */
|
||||||
|
#define GITEA_BASE_URL "http://gitea.vayrette.com"
|
||||||
|
#define GITEA_REPO "seb/saltylab-firmware"
|
||||||
|
#define GITEA_API_TIMEOUT_MS 10000
|
||||||
|
|
||||||
|
/* Version check interval */
|
||||||
|
#define VERSION_CHECK_PERIOD_MS (30u * 60u * 1000u) /* 30 minutes */
|
||||||
|
|
||||||
|
/* Max URL/version string lengths */
|
||||||
|
#define OTA_URL_MAX 384
|
||||||
|
#define OTA_VER_MAX 32
|
||||||
|
#define OTA_SHA256_MAX 65
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool available;
|
||||||
|
char version[OTA_VER_MAX]; /* remote version string, e.g. "1.2.3" */
|
||||||
|
char download_url[OTA_URL_MAX]; /* direct download URL for .bin */
|
||||||
|
char sha256[OTA_SHA256_MAX]; /* hex SHA256 (from .sha256 asset), or "" */
|
||||||
|
} ota_update_info_t;
|
||||||
|
|
||||||
|
/* Shared state — written by gitea_ota_check_task, read by display/OTA tasks */
|
||||||
|
extern ota_update_info_t g_balance_update;
|
||||||
|
extern ota_update_info_t g_io_update;
|
||||||
|
|
||||||
|
/* Initialize WiFi and start version check task */
|
||||||
|
void gitea_ota_init(void);
|
||||||
|
|
||||||
|
/* One-shot sync check (can be called from any task) */
|
||||||
|
void gitea_ota_check_now(void);
|
||||||
@ -1,15 +1,11 @@
|
|||||||
/* main.c — ESP32-S3 BALANCE app_main (bd-66hx)
|
/* main.c — ESP32-S3 BALANCE app_main (bd-66hx + OTA beads) */
|
||||||
*
|
|
||||||
* Initializes Orin serial and VESC CAN TWAI, creates tasks:
|
|
||||||
* orin_rx — parse incoming Orin commands
|
|
||||||
* orin_tx — transmit queued serial frames
|
|
||||||
* vesc_rx — receive VESC CAN telemetry, proxy to Orin
|
|
||||||
* telem — periodic TELEM_STATUS to Orin @ 10 Hz
|
|
||||||
* drive — apply Orin drive commands to VESCs via CAN
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "orin_serial.h"
|
#include "orin_serial.h"
|
||||||
#include "vesc_can.h"
|
#include "vesc_can.h"
|
||||||
|
#include "gitea_ota.h"
|
||||||
|
#include "ota_self.h"
|
||||||
|
#include "uart_ota.h"
|
||||||
|
#include "ota_display.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
@ -86,7 +82,10 @@ static void drive_task(void *arg)
|
|||||||
|
|
||||||
void app_main(void)
|
void app_main(void)
|
||||||
{
|
{
|
||||||
ESP_LOGI(TAG, "ESP32-S3 BALANCE bd-66hx starting");
|
ESP_LOGI(TAG, "ESP32-S3 BALANCE starting");
|
||||||
|
|
||||||
|
/* OTA rollback health check — must be called within OTA_ROLLBACK_WINDOW_S */
|
||||||
|
ota_self_health_check();
|
||||||
|
|
||||||
/* Init peripherals */
|
/* Init peripherals */
|
||||||
orin_serial_init();
|
orin_serial_init();
|
||||||
@ -106,6 +105,10 @@ void app_main(void)
|
|||||||
xTaskCreate(telem_task, "telem", 2048, NULL, 5, NULL);
|
xTaskCreate(telem_task, "telem", 2048, NULL, 5, NULL);
|
||||||
xTaskCreate(drive_task, "drive", 2048, NULL, 8, NULL);
|
xTaskCreate(drive_task, "drive", 2048, NULL, 8, NULL);
|
||||||
|
|
||||||
|
/* OTA subsystem — WiFi version checker + display overlay */
|
||||||
|
gitea_ota_init();
|
||||||
|
ota_display_init();
|
||||||
|
|
||||||
ESP_LOGI(TAG, "all tasks started");
|
ESP_LOGI(TAG, "all tasks started");
|
||||||
/* app_main returns — FreeRTOS scheduler continues */
|
/* app_main returns — FreeRTOS scheduler continues */
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
/* orin_serial.c — Orin↔ESP32-S3 serial protocol implementation (bd-66hx)
|
/* orin_serial.c — Orin↔ESP32-S3 serial protocol (bd-66hx + bd-1s1s OTA cmds) */
|
||||||
*
|
|
||||||
* Implements the binary framing protocol matching bd-wim1 (Orin side).
|
|
||||||
* CRC8-SMBUS: poly=0x07, init=0x00, covers LEN+TYPE+PAYLOAD bytes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "orin_serial.h"
|
#include "orin_serial.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "gitea_ota.h"
|
||||||
|
#include "ota_self.h"
|
||||||
|
#include "uart_ota.h"
|
||||||
|
#include "version.h"
|
||||||
#include "driver/uart.h"
|
#include "driver/uart.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/queue.h"
|
#include "freertos/queue.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
static const char *TAG = "orin";
|
static const char *TAG = "orin";
|
||||||
|
|
||||||
@ -206,6 +207,46 @@ static void dispatch_cmd(uint8_t type, const uint8_t *payload, uint8_t len,
|
|||||||
orin_send_ack(tx_q, type);
|
orin_send_ack(tx_q, type);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case CMD_OTA_CHECK:
|
||||||
|
/* Trigger an immediate Gitea version check */
|
||||||
|
gitea_ota_check_now();
|
||||||
|
orin_send_version_info(tx_q, OTA_TARGET_BALANCE,
|
||||||
|
BALANCE_FW_VERSION,
|
||||||
|
g_balance_update.available
|
||||||
|
? g_balance_update.version : "");
|
||||||
|
orin_send_version_info(tx_q, OTA_TARGET_IO,
|
||||||
|
IO_FW_VERSION,
|
||||||
|
g_io_update.available
|
||||||
|
? g_io_update.version : "");
|
||||||
|
orin_send_ack(tx_q, type);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CMD_OTA_UPDATE:
|
||||||
|
if (len < 1u) { orin_send_nack(tx_q, type, ERR_BAD_LEN); break; }
|
||||||
|
{
|
||||||
|
uint8_t target = payload[0];
|
||||||
|
bool triggered = false;
|
||||||
|
if (target == OTA_TARGET_IO || target == OTA_TARGET_BOTH) {
|
||||||
|
if (!uart_ota_trigger()) {
|
||||||
|
orin_send_nack(tx_q, type,
|
||||||
|
g_io_update.available ? ERR_OTA_BUSY : ERR_OTA_NO_UPDATE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
triggered = true;
|
||||||
|
}
|
||||||
|
if (target == OTA_TARGET_BALANCE || target == OTA_TARGET_BOTH) {
|
||||||
|
if (!ota_self_trigger()) {
|
||||||
|
if (!triggered) {
|
||||||
|
orin_send_nack(tx_q, type,
|
||||||
|
g_balance_update.available ? ERR_OTA_BUSY : ERR_OTA_NO_UPDATE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
orin_send_ack(tx_q, type);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
ESP_LOGW(TAG, "unknown cmd type=0x%02x", type);
|
ESP_LOGW(TAG, "unknown cmd type=0x%02x", type);
|
||||||
break;
|
break;
|
||||||
@ -290,3 +331,24 @@ void orin_serial_tx_task(void *arg)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── OTA telemetry helpers (bd-1s1s) ── */
|
||||||
|
|
||||||
|
void orin_send_ota_status(QueueHandle_t q, uint8_t target,
|
||||||
|
uint8_t state, uint8_t progress, uint8_t err)
|
||||||
|
{
|
||||||
|
/* TELEM_OTA_STATUS: uint8 target, uint8 state, uint8 progress, uint8 err */
|
||||||
|
uint8_t p[4] = {target, state, progress, err};
|
||||||
|
enqueue(q, TELEM_OTA_STATUS, p, 4u);
|
||||||
|
}
|
||||||
|
|
||||||
|
void orin_send_version_info(QueueHandle_t q, uint8_t target,
|
||||||
|
const char *current, const char *available)
|
||||||
|
{
|
||||||
|
/* TELEM_VERSION_INFO: uint8 target, char current[16], char available[16] */
|
||||||
|
uint8_t p[33];
|
||||||
|
p[0] = target;
|
||||||
|
strncpy((char *)&p[1], current, 16); p[16] = '\0';
|
||||||
|
strncpy((char *)&p[17], available ? available : "", 16); p[32] = '\0';
|
||||||
|
enqueue(q, TELEM_VERSION_INFO, p, 33u);
|
||||||
|
}
|
||||||
|
|||||||
@ -29,14 +29,27 @@
|
|||||||
#define TELEM_STATUS 0x80u /* status @ 10 Hz */
|
#define TELEM_STATUS 0x80u /* status @ 10 Hz */
|
||||||
#define TELEM_VESC_LEFT 0x81u /* VESC ID 56 telemetry @ 10 Hz */
|
#define TELEM_VESC_LEFT 0x81u /* VESC ID 56 telemetry @ 10 Hz */
|
||||||
#define TELEM_VESC_RIGHT 0x82u /* VESC ID 68 telemetry @ 10 Hz */
|
#define TELEM_VESC_RIGHT 0x82u /* VESC ID 68 telemetry @ 10 Hz */
|
||||||
|
#define TELEM_OTA_STATUS 0x83u /* OTA state + progress (bd-1s1s) */
|
||||||
|
#define TELEM_VERSION_INFO 0x84u /* firmware version report (bd-1s1s) */
|
||||||
#define RESP_ACK 0xA0u
|
#define RESP_ACK 0xA0u
|
||||||
#define RESP_NACK 0xA1u
|
#define RESP_NACK 0xA1u
|
||||||
|
|
||||||
|
/* ── OTA commands (Orin → ESP32, bd-1s1s) ── */
|
||||||
|
#define CMD_OTA_CHECK 0x10u /* no payload: trigger Gitea version check */
|
||||||
|
#define CMD_OTA_UPDATE 0x11u /* uint8 target: 0=balance, 1=io, 2=both */
|
||||||
|
|
||||||
|
/* ── OTA target constants ── */
|
||||||
|
#define OTA_TARGET_BALANCE 0x00u
|
||||||
|
#define OTA_TARGET_IO 0x01u
|
||||||
|
#define OTA_TARGET_BOTH 0x02u
|
||||||
|
|
||||||
/* ── NACK error codes ── */
|
/* ── NACK error codes ── */
|
||||||
#define ERR_BAD_CRC 0x01u
|
#define ERR_BAD_CRC 0x01u
|
||||||
#define ERR_BAD_LEN 0x02u
|
#define ERR_BAD_LEN 0x02u
|
||||||
#define ERR_ESTOP_ACTIVE 0x03u
|
#define ERR_ESTOP_ACTIVE 0x03u
|
||||||
#define ERR_DISARMED 0x04u
|
#define ERR_DISARMED 0x04u
|
||||||
|
#define ERR_OTA_BUSY 0x05u
|
||||||
|
#define ERR_OTA_NO_UPDATE 0x06u
|
||||||
|
|
||||||
/* ── Balance state (mirrored from TELEM_STATUS.balance_state) ── */
|
/* ── Balance state (mirrored from TELEM_STATUS.balance_state) ── */
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@ -92,3 +105,9 @@ void orin_send_vesc(QueueHandle_t q, uint8_t telem_type,
|
|||||||
int16_t current_ma, uint16_t temp_c_x10);
|
int16_t current_ma, uint16_t temp_c_x10);
|
||||||
void orin_send_ack(QueueHandle_t q, uint8_t cmd_type);
|
void orin_send_ack(QueueHandle_t q, uint8_t cmd_type);
|
||||||
void orin_send_nack(QueueHandle_t q, uint8_t cmd_type, uint8_t err);
|
void orin_send_nack(QueueHandle_t q, uint8_t cmd_type, uint8_t err);
|
||||||
|
|
||||||
|
/* OTA telemetry helpers (bd-1s1s) */
|
||||||
|
void orin_send_ota_status(QueueHandle_t q, uint8_t target,
|
||||||
|
uint8_t state, uint8_t progress, uint8_t err);
|
||||||
|
void orin_send_version_info(QueueHandle_t q, uint8_t target,
|
||||||
|
const char *current, const char *available);
|
||||||
|
|||||||
150
esp32s3/balance/main/ota_display.c
Normal file
150
esp32s3/balance/main/ota_display.c
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
/* ota_display.c — OTA notification/progress UI on GC9A01 (bd-1yr8)
|
||||||
|
*
|
||||||
|
* Renders OTA state overlaid on the 240×240 round HUD display:
|
||||||
|
* - BADGE: small dot on top-right when update available (idle state)
|
||||||
|
* - UPDATE SCREEN: version compare, Update Balance / Update IO / Update All
|
||||||
|
* - PROGRESS: arc around display perimeter + % + status text
|
||||||
|
* - ERROR: red banner + "RETRY" prompt
|
||||||
|
*
|
||||||
|
* The display_draw_* primitives must be provided by the GC9A01 driver.
|
||||||
|
* Actual SPI driver implementation is in a separate driver bead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ota_display.h"
|
||||||
|
#include "gitea_ota.h"
|
||||||
|
#include "version.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static const char *TAG = "ota_disp";
|
||||||
|
|
||||||
|
/* Display centre and radius for the 240×240 GC9A01 */
|
||||||
|
#define CX 120
|
||||||
|
#define CY 120
|
||||||
|
#define RAD 110
|
||||||
|
|
||||||
|
/* ── Availability badge: 8×8 dot at top-right of display ── */
|
||||||
|
static void draw_badge(bool balance_avail, bool io_avail)
|
||||||
|
{
|
||||||
|
uint16_t col = (balance_avail || io_avail) ? COL_ORANGE : COL_BG;
|
||||||
|
display_fill_rect(200, 15, 12, 12, col);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Progress arc: sweeps 0→360° proportional to progress% ── */
|
||||||
|
static void draw_progress_arc(uint8_t pct, uint16_t color)
|
||||||
|
{
|
||||||
|
int end_deg = (int)(360 * pct / 100);
|
||||||
|
display_draw_arc(CX, CY, RAD, 0, end_deg, 6, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status banner: 2 lines of text centred on display ── */
|
||||||
|
static void draw_status(const char *line1, const char *line2,
|
||||||
|
uint16_t fg, uint16_t bg)
|
||||||
|
{
|
||||||
|
display_fill_rect(20, 90, 200, 60, bg);
|
||||||
|
if (line1 && line1[0])
|
||||||
|
display_draw_string(CX - (int)(strlen(line1) * 6 / 2), 96,
|
||||||
|
line1, fg, bg);
|
||||||
|
if (line2 && line2[0])
|
||||||
|
display_draw_string(CX - (int)(strlen(line2) * 6 / 2), 116,
|
||||||
|
line2, fg, bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Main render logic ── */
|
||||||
|
void ota_display_update(void)
|
||||||
|
{
|
||||||
|
/* Determine dominant OTA state */
|
||||||
|
ota_self_state_t self = g_ota_self_state;
|
||||||
|
uart_ota_send_state_t io_s = g_uart_ota_state;
|
||||||
|
|
||||||
|
switch (self) {
|
||||||
|
case OTA_SELF_DOWNLOADING:
|
||||||
|
case OTA_SELF_VERIFYING:
|
||||||
|
case OTA_SELF_APPLYING: {
|
||||||
|
/* Balance self-update in progress */
|
||||||
|
char pct_str[16];
|
||||||
|
snprintf(pct_str, sizeof(pct_str), "%d%%", g_ota_self_progress);
|
||||||
|
const char *phase = (self == OTA_SELF_VERIFYING) ? "Verifying..." :
|
||||||
|
(self == OTA_SELF_APPLYING) ? "Applying..." :
|
||||||
|
"Downloading...";
|
||||||
|
draw_progress_arc(g_ota_self_progress, COL_BLUE);
|
||||||
|
draw_status("Updating Balance", pct_str, COL_WHITE, COL_BG);
|
||||||
|
ESP_LOGD(TAG, "balance OTA %s %d%%", phase, g_ota_self_progress);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case OTA_SELF_REBOOTING:
|
||||||
|
draw_status("Update complete", "Rebooting...", COL_GREEN, COL_BG);
|
||||||
|
return;
|
||||||
|
case OTA_SELF_FAILED:
|
||||||
|
draw_progress_arc(0, COL_RED);
|
||||||
|
draw_status("Balance update", "FAILED RETRY?", COL_RED, COL_BG);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (io_s) {
|
||||||
|
case UART_OTA_S_DOWNLOADING:
|
||||||
|
draw_progress_arc(g_uart_ota_progress, COL_YELLOW);
|
||||||
|
draw_status("Downloading IO", "firmware...", COL_WHITE, COL_BG);
|
||||||
|
return;
|
||||||
|
case UART_OTA_S_SENDING: {
|
||||||
|
char pct_str[16];
|
||||||
|
snprintf(pct_str, sizeof(pct_str), "%d%%", g_uart_ota_progress);
|
||||||
|
draw_progress_arc(g_uart_ota_progress, COL_YELLOW);
|
||||||
|
draw_status("Updating IO", pct_str, COL_WHITE, COL_BG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case UART_OTA_S_DONE:
|
||||||
|
draw_status("IO update done", "", COL_GREEN, COL_BG);
|
||||||
|
return;
|
||||||
|
case UART_OTA_S_FAILED:
|
||||||
|
draw_progress_arc(0, COL_RED);
|
||||||
|
draw_status("IO update", "FAILED RETRY?", COL_RED, COL_BG);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Idle — show badge if update available */
|
||||||
|
bool bal_avail = g_balance_update.available;
|
||||||
|
bool io_avail = g_io_update.available;
|
||||||
|
draw_badge(bal_avail, io_avail);
|
||||||
|
|
||||||
|
if (bal_avail || io_avail) {
|
||||||
|
/* Show available versions on display when idle */
|
||||||
|
char verline[32];
|
||||||
|
if (bal_avail) {
|
||||||
|
snprintf(verline, sizeof(verline), "Bal v%s rdy",
|
||||||
|
g_balance_update.version);
|
||||||
|
draw_status(verline, io_avail ? "IO update rdy" : "",
|
||||||
|
COL_ORANGE, COL_BG);
|
||||||
|
} else if (io_avail) {
|
||||||
|
snprintf(verline, sizeof(verline), "IO v%s rdy",
|
||||||
|
g_io_update.version);
|
||||||
|
draw_status(verline, "", COL_ORANGE, COL_BG);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* Clear OTA overlay area */
|
||||||
|
display_fill_rect(20, 90, 200, 60, COL_BG);
|
||||||
|
draw_badge(false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Background display task (5 Hz) ── */
|
||||||
|
static void ota_display_task(void *arg)
|
||||||
|
{
|
||||||
|
for (;;) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(200));
|
||||||
|
ota_display_update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ota_display_init(void)
|
||||||
|
{
|
||||||
|
xTaskCreate(ota_display_task, "ota_disp", 2048, NULL, 3, NULL);
|
||||||
|
ESP_LOGI(TAG, "OTA display task started");
|
||||||
|
}
|
||||||
33
esp32s3/balance/main/ota_display.h
Normal file
33
esp32s3/balance/main/ota_display.h
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
/* ota_display.h — OTA notification UI on GC9A01 round LCD (bd-1yr8)
|
||||||
|
*
|
||||||
|
* GC9A01 240×240 round display via SPI (IO12 CS, IO11 DC, IO10 RST, IO9 BL).
|
||||||
|
* Calls into display_draw_* primitives (provided by display driver layer).
|
||||||
|
* This module owns the "OTA notification overlay" rendered over the HUD.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include "ota_self.h"
|
||||||
|
#include "uart_ota.h"
|
||||||
|
|
||||||
|
/* ── Display primitives API (must be provided by display driver) ── */
|
||||||
|
void display_fill_rect(int x, int y, int w, int h, uint16_t rgb565);
|
||||||
|
void display_draw_string(int x, int y, const char *str, uint16_t fg, uint16_t bg);
|
||||||
|
void display_draw_arc(int cx, int cy, int r, int start_deg, int end_deg,
|
||||||
|
int thickness, uint16_t color);
|
||||||
|
|
||||||
|
/* ── Colour palette (RGB565) ── */
|
||||||
|
#define COL_BG 0x0000u /* black */
|
||||||
|
#define COL_WHITE 0xFFFFu
|
||||||
|
#define COL_GREEN 0x07E0u
|
||||||
|
#define COL_YELLOW 0xFFE0u
|
||||||
|
#define COL_RED 0xF800u
|
||||||
|
#define COL_BLUE 0x001Fu
|
||||||
|
#define COL_ORANGE 0xFD20u
|
||||||
|
|
||||||
|
/* ── OTA display task: runs at 5 Hz, overlays OTA state on HUD ── */
|
||||||
|
void ota_display_init(void);
|
||||||
|
|
||||||
|
/* Called from main loop or display task to render the OTA overlay */
|
||||||
|
void ota_display_update(void);
|
||||||
183
esp32s3/balance/main/ota_self.c
Normal file
183
esp32s3/balance/main/ota_self.c
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
/* ota_self.c — Balance self-OTA (bd-18nb)
|
||||||
|
*
|
||||||
|
* Uses esp_https_ota / esp_ota_ops to download from Gitea release URL,
|
||||||
|
* stream-verify SHA256 with mbedTLS, set new boot partition, and reboot.
|
||||||
|
* CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE in sdkconfig allows auto-rollback
|
||||||
|
* if the new image doesn't call esp_ota_mark_app_valid_cancel_rollback()
|
||||||
|
* within OTA_ROLLBACK_WINDOW_S seconds.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ota_self.h"
|
||||||
|
#include "gitea_ota.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_ota_ops.h"
|
||||||
|
#include "esp_http_client.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "mbedtls/sha256.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
static const char *TAG = "ota_self";
|
||||||
|
|
||||||
|
volatile ota_self_state_t g_ota_self_state = OTA_SELF_IDLE;
|
||||||
|
volatile uint8_t g_ota_self_progress = 0;
|
||||||
|
|
||||||
|
#define OTA_CHUNK_SIZE 4096
|
||||||
|
|
||||||
|
/* ── SHA256 verify helper ── */
|
||||||
|
static bool sha256_matches(const uint8_t *digest, const char *expected_hex)
|
||||||
|
{
|
||||||
|
if (!expected_hex || expected_hex[0] == '\0') {
|
||||||
|
ESP_LOGW(TAG, "no SHA256 to verify — skipping");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
char got[65] = {0};
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
snprintf(&got[i*2], 3, "%02x", digest[i]);
|
||||||
|
}
|
||||||
|
bool ok = (strncasecmp(got, expected_hex, 64) == 0);
|
||||||
|
if (!ok) {
|
||||||
|
ESP_LOGE(TAG, "SHA256 mismatch: got=%s exp=%s", got, expected_hex);
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── OTA download + flash task ── */
|
||||||
|
static void ota_self_task(void *arg)
|
||||||
|
{
|
||||||
|
const char *url = g_balance_update.download_url;
|
||||||
|
const char *sha256 = g_balance_update.sha256;
|
||||||
|
|
||||||
|
g_ota_self_state = OTA_SELF_DOWNLOADING;
|
||||||
|
g_ota_self_progress = 0;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "OTA start: %s", url);
|
||||||
|
|
||||||
|
esp_ota_handle_t handle = 0;
|
||||||
|
const esp_partition_t *ota_part = esp_ota_get_next_update_partition(NULL);
|
||||||
|
if (!ota_part) {
|
||||||
|
ESP_LOGE(TAG, "no OTA partition");
|
||||||
|
g_ota_self_state = OTA_SELF_FAILED;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = esp_ota_begin(ota_part, OTA_WITH_SEQUENTIAL_WRITES, &handle);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "ota_begin: %s", esp_err_to_name(err));
|
||||||
|
g_ota_self_state = OTA_SELF_FAILED;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Setup HTTP client */
|
||||||
|
esp_http_client_config_t hcfg = {
|
||||||
|
.url = url,
|
||||||
|
.timeout_ms = 30000,
|
||||||
|
.buffer_size = OTA_CHUNK_SIZE,
|
||||||
|
.skip_cert_common_name_check = true,
|
||||||
|
};
|
||||||
|
esp_http_client_handle_t client = esp_http_client_init(&hcfg);
|
||||||
|
err = esp_http_client_open(client, 0);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "http_open: %s", esp_err_to_name(err));
|
||||||
|
esp_ota_abort(handle);
|
||||||
|
esp_http_client_cleanup(client);
|
||||||
|
g_ota_self_state = OTA_SELF_FAILED;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int content_len = esp_http_client_fetch_headers(client);
|
||||||
|
ESP_LOGI(TAG, "content-length: %d", content_len);
|
||||||
|
|
||||||
|
mbedtls_sha256_context sha_ctx;
|
||||||
|
mbedtls_sha256_init(&sha_ctx);
|
||||||
|
mbedtls_sha256_starts(&sha_ctx, 0); /* 0 = SHA-256 */
|
||||||
|
|
||||||
|
static uint8_t buf[OTA_CHUNK_SIZE];
|
||||||
|
int total = 0;
|
||||||
|
int rd;
|
||||||
|
while ((rd = esp_http_client_read(client, (char *)buf, sizeof(buf))) > 0) {
|
||||||
|
mbedtls_sha256_update(&sha_ctx, buf, rd);
|
||||||
|
err = esp_ota_write(handle, buf, rd);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "ota_write: %s", esp_err_to_name(err));
|
||||||
|
esp_ota_abort(handle);
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
total += rd;
|
||||||
|
if (content_len > 0) {
|
||||||
|
g_ota_self_progress = (uint8_t)((total * 100) / content_len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
esp_http_client_close(client);
|
||||||
|
|
||||||
|
/* Verify SHA256 */
|
||||||
|
g_ota_self_state = OTA_SELF_VERIFYING;
|
||||||
|
uint8_t digest[32];
|
||||||
|
mbedtls_sha256_finish(&sha_ctx, digest);
|
||||||
|
if (!sha256_matches(digest, sha256)) {
|
||||||
|
ESP_LOGE(TAG, "SHA256 verification failed");
|
||||||
|
esp_ota_abort(handle);
|
||||||
|
g_ota_self_state = OTA_SELF_FAILED;
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Finalize + set boot partition */
|
||||||
|
g_ota_self_state = OTA_SELF_APPLYING;
|
||||||
|
err = esp_ota_end(handle);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "ota_end: %s", esp_err_to_name(err));
|
||||||
|
g_ota_self_state = OTA_SELF_FAILED;
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = esp_ota_set_boot_partition(ota_part);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "set_boot_partition: %s", esp_err_to_name(err));
|
||||||
|
g_ota_self_state = OTA_SELF_FAILED;
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_ota_self_state = OTA_SELF_REBOOTING;
|
||||||
|
g_ota_self_progress = 100;
|
||||||
|
ESP_LOGI(TAG, "OTA success — rebooting");
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
esp_restart();
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
mbedtls_sha256_free(&sha_ctx);
|
||||||
|
esp_http_client_cleanup(client);
|
||||||
|
handle = 0;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ota_self_trigger(void)
|
||||||
|
{
|
||||||
|
if (!g_balance_update.available) {
|
||||||
|
ESP_LOGW(TAG, "no update available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (g_ota_self_state != OTA_SELF_IDLE) {
|
||||||
|
ESP_LOGW(TAG, "OTA already in progress (state=%d)", g_ota_self_state);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
xTaskCreate(ota_self_task, "ota_self", 8192, NULL, 5, NULL);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ota_self_health_check(void)
|
||||||
|
{
|
||||||
|
/* Mark running image as valid — prevents rollback */
|
||||||
|
esp_err_t err = esp_ota_mark_app_valid_cancel_rollback();
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "image marked valid");
|
||||||
|
} else if (err == ESP_ERR_NOT_SUPPORTED) {
|
||||||
|
/* Not an OTA image (e.g., flashed via JTAG) — ignore */
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "mark_valid: %s", esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
34
esp32s3/balance/main/ota_self.h
Normal file
34
esp32s3/balance/main/ota_self.h
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
/* ota_self.h — Balance self-OTA (bd-18nb)
|
||||||
|
*
|
||||||
|
* Downloads balance-firmware.bin from Gitea release URL to the inactive
|
||||||
|
* OTA partition, verifies SHA256, sets boot partition, reboots.
|
||||||
|
* Auto-rollback if health check not called within ROLLBACK_WINDOW_S seconds.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#define OTA_ROLLBACK_WINDOW_S 30
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
OTA_SELF_IDLE = 0,
|
||||||
|
OTA_SELF_CHECKING, /* (unused — gitea_ota handles this) */
|
||||||
|
OTA_SELF_DOWNLOADING,
|
||||||
|
OTA_SELF_VERIFYING,
|
||||||
|
OTA_SELF_APPLYING,
|
||||||
|
OTA_SELF_REBOOTING,
|
||||||
|
OTA_SELF_FAILED,
|
||||||
|
} ota_self_state_t;
|
||||||
|
|
||||||
|
extern volatile ota_self_state_t g_ota_self_state;
|
||||||
|
extern volatile uint8_t g_ota_self_progress; /* 0-100 % */
|
||||||
|
|
||||||
|
/* Trigger a Balance self-update.
|
||||||
|
* Uses g_balance_update (from gitea_ota). Non-blocking: starts in a task.
|
||||||
|
* Returns false if no update available or OTA already in progress. */
|
||||||
|
bool ota_self_trigger(void);
|
||||||
|
|
||||||
|
/* Called from app_main after boot to mark the running image as valid.
|
||||||
|
* Must be called within OTA_ROLLBACK_WINDOW_S after boot or rollback fires. */
|
||||||
|
void ota_self_health_check(void);
|
||||||
241
esp32s3/balance/main/uart_ota.c
Normal file
241
esp32s3/balance/main/uart_ota.c
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
/* uart_ota.c — UART OTA sender: Balance→IO board (bd-21hv)
|
||||||
|
*
|
||||||
|
* Downloads io-firmware.bin from Gitea, then sends to IO board via UART1.
|
||||||
|
* IO board must update itself BEFORE Balance self-update (per spec).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uart_ota.h"
|
||||||
|
#include "gitea_ota.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_http_client.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "mbedtls/sha256.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
static const char *TAG = "uart_ota";
|
||||||
|
|
||||||
|
volatile uart_ota_send_state_t g_uart_ota_state = UART_OTA_S_IDLE;
|
||||||
|
volatile uint8_t g_uart_ota_progress = 0;
|
||||||
|
|
||||||
|
/* ── CRC8-SMBUS ── */
|
||||||
|
static uint8_t crc8(const uint8_t *d, uint16_t len)
|
||||||
|
{
|
||||||
|
uint8_t crc = 0;
|
||||||
|
for (uint16_t i = 0; i < len; i++) {
|
||||||
|
crc ^= d[i];
|
||||||
|
for (uint8_t b = 0; b < 8; b++)
|
||||||
|
crc = (crc & 0x80u) ? (uint8_t)((crc << 1u) ^ 0x07u) : (uint8_t)(crc << 1u);
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Build and send one UART OTA frame ── */
|
||||||
|
static void send_frame(uint8_t type, uint16_t seq,
|
||||||
|
const uint8_t *payload, uint16_t plen)
|
||||||
|
{
|
||||||
|
/* [TYPE:1][SEQ:2 BE][LEN:2 BE][PAYLOAD][CRC8:1] */
|
||||||
|
uint8_t hdr[5];
|
||||||
|
hdr[0] = type;
|
||||||
|
hdr[1] = (uint8_t)(seq >> 8u);
|
||||||
|
hdr[2] = (uint8_t)(seq);
|
||||||
|
hdr[3] = (uint8_t)(plen >> 8u);
|
||||||
|
hdr[4] = (uint8_t)(plen);
|
||||||
|
|
||||||
|
/* CRC over hdr + payload */
|
||||||
|
uint8_t crc_buf[5 + OTA_UART_CHUNK_SIZE];
|
||||||
|
memcpy(crc_buf, hdr, 5);
|
||||||
|
if (plen > 0 && payload) memcpy(crc_buf + 5, payload, plen);
|
||||||
|
uint8_t crc = crc8(crc_buf, (uint16_t)(5 + plen));
|
||||||
|
|
||||||
|
uart_write_bytes(UART_OTA_PORT, (char *)hdr, 5);
|
||||||
|
if (plen > 0 && payload)
|
||||||
|
uart_write_bytes(UART_OTA_PORT, (char *)payload, plen);
|
||||||
|
uart_write_bytes(UART_OTA_PORT, (char *)&crc, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Wait for ACK/NACK from IO board ── */
|
||||||
|
static bool wait_ack(uint16_t expected_seq)
|
||||||
|
{
|
||||||
|
/* Response frame: [TYPE:1][SEQ:2][LEN:2][PAYLOAD][CRC:1] */
|
||||||
|
uint8_t buf[16];
|
||||||
|
int timeout = OTA_UART_ACK_TIMEOUT_MS;
|
||||||
|
int got = 0;
|
||||||
|
|
||||||
|
while (timeout > 0 && got < 6) {
|
||||||
|
int r = uart_read_bytes(UART_OTA_PORT, buf + got, 1, pdMS_TO_TICKS(50));
|
||||||
|
if (r > 0) got++;
|
||||||
|
else timeout -= 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (got < 3) return false;
|
||||||
|
|
||||||
|
uint8_t type = buf[0];
|
||||||
|
uint16_t seq = (uint16_t)((buf[1] << 8u) | buf[2]);
|
||||||
|
|
||||||
|
if (type == UART_OTA_ACK && seq == expected_seq) return true;
|
||||||
|
if (type == UART_OTA_NACK) {
|
||||||
|
uint8_t err = (got >= 6) ? buf[5] : 0;
|
||||||
|
ESP_LOGW(TAG, "NACK seq=%u err=%u", seq, err);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Download firmware to RAM buffer (max 1.75 MB) ── */
|
||||||
|
static uint8_t *download_io_firmware(uint32_t *out_size)
|
||||||
|
{
|
||||||
|
const char *url = g_io_update.download_url;
|
||||||
|
ESP_LOGI(TAG, "downloading IO fw: %s", url);
|
||||||
|
|
||||||
|
esp_http_client_config_t cfg = {
|
||||||
|
.url = url, .timeout_ms = 30000,
|
||||||
|
.skip_cert_common_name_check = true,
|
||||||
|
};
|
||||||
|
esp_http_client_handle_t client = esp_http_client_init(&cfg);
|
||||||
|
if (esp_http_client_open(client, 0) != ESP_OK) {
|
||||||
|
esp_http_client_cleanup(client);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int content_len = esp_http_client_fetch_headers(client);
|
||||||
|
if (content_len <= 0 || content_len > (int)(0x1B0000)) {
|
||||||
|
ESP_LOGE(TAG, "bad content-length: %d", content_len);
|
||||||
|
esp_http_client_cleanup(client);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t *buf = malloc(content_len);
|
||||||
|
if (!buf) {
|
||||||
|
ESP_LOGE(TAG, "malloc %d failed", content_len);
|
||||||
|
esp_http_client_cleanup(client);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int total = 0, rd;
|
||||||
|
while ((rd = esp_http_client_read(client, (char *)buf + total,
|
||||||
|
content_len - total)) > 0) {
|
||||||
|
total += rd;
|
||||||
|
g_uart_ota_progress = (uint8_t)((total * 50) / content_len); /* 0-50% for download */
|
||||||
|
}
|
||||||
|
esp_http_client_cleanup(client);
|
||||||
|
|
||||||
|
if (total != content_len) {
|
||||||
|
free(buf);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
*out_size = (uint32_t)total;
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── UART OTA send task ── */
|
||||||
|
static void uart_ota_task(void *arg)
|
||||||
|
{
|
||||||
|
g_uart_ota_state = UART_OTA_S_DOWNLOADING;
|
||||||
|
g_uart_ota_progress = 0;
|
||||||
|
|
||||||
|
uint32_t fw_size = 0;
|
||||||
|
uint8_t *fw = download_io_firmware(&fw_size);
|
||||||
|
if (!fw) {
|
||||||
|
ESP_LOGE(TAG, "download failed");
|
||||||
|
g_uart_ota_state = UART_OTA_S_FAILED;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compute SHA256 of downloaded firmware */
|
||||||
|
uint8_t digest[32];
|
||||||
|
mbedtls_sha256_context sha;
|
||||||
|
mbedtls_sha256_init(&sha);
|
||||||
|
mbedtls_sha256_starts(&sha, 0);
|
||||||
|
mbedtls_sha256_update(&sha, fw, fw_size);
|
||||||
|
mbedtls_sha256_finish(&sha, digest);
|
||||||
|
mbedtls_sha256_free(&sha);
|
||||||
|
|
||||||
|
g_uart_ota_state = UART_OTA_S_SENDING;
|
||||||
|
|
||||||
|
/* Send OTA_BEGIN: uint32 size + uint8[32] sha256 */
|
||||||
|
uint8_t begin_payload[36];
|
||||||
|
begin_payload[0] = (uint8_t)(fw_size >> 24u);
|
||||||
|
begin_payload[1] = (uint8_t)(fw_size >> 16u);
|
||||||
|
begin_payload[2] = (uint8_t)(fw_size >> 8u);
|
||||||
|
begin_payload[3] = (uint8_t)(fw_size);
|
||||||
|
memcpy(&begin_payload[4], digest, 32);
|
||||||
|
|
||||||
|
for (int retry = 0; retry < OTA_UART_MAX_RETRIES; retry++) {
|
||||||
|
send_frame(UART_OTA_BEGIN, 0, begin_payload, 36);
|
||||||
|
if (wait_ack(0)) goto send_data;
|
||||||
|
ESP_LOGW(TAG, "BEGIN retry %d", retry);
|
||||||
|
}
|
||||||
|
ESP_LOGE(TAG, "BEGIN failed");
|
||||||
|
free(fw);
|
||||||
|
g_uart_ota_state = UART_OTA_S_FAILED;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
return;
|
||||||
|
|
||||||
|
send_data: {
|
||||||
|
uint32_t offset = 0;
|
||||||
|
uint16_t seq = 1;
|
||||||
|
while (offset < fw_size) {
|
||||||
|
uint16_t chunk = (uint16_t)((fw_size - offset) < OTA_UART_CHUNK_SIZE
|
||||||
|
? (fw_size - offset) : OTA_UART_CHUNK_SIZE);
|
||||||
|
bool acked = false;
|
||||||
|
for (int retry = 0; retry < OTA_UART_MAX_RETRIES; retry++) {
|
||||||
|
send_frame(UART_OTA_DATA, seq, fw + offset, chunk);
|
||||||
|
if (wait_ack(seq)) { acked = true; break; }
|
||||||
|
ESP_LOGW(TAG, "DATA seq=%u retry=%d", seq, retry);
|
||||||
|
}
|
||||||
|
if (!acked) {
|
||||||
|
ESP_LOGE(TAG, "DATA seq=%u failed", seq);
|
||||||
|
send_frame(UART_OTA_ABORT, seq, NULL, 0);
|
||||||
|
free(fw);
|
||||||
|
g_uart_ota_state = UART_OTA_S_FAILED;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
offset += chunk;
|
||||||
|
seq++;
|
||||||
|
/* 50-100% for sending phase */
|
||||||
|
g_uart_ota_progress = (uint8_t)(50u + (offset * 50u) / fw_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Send OTA_END */
|
||||||
|
for (int retry = 0; retry < OTA_UART_MAX_RETRIES; retry++) {
|
||||||
|
send_frame(UART_OTA_END, seq, NULL, 0);
|
||||||
|
if (wait_ack(seq)) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(fw);
|
||||||
|
g_uart_ota_progress = 100;
|
||||||
|
g_uart_ota_state = UART_OTA_S_DONE;
|
||||||
|
ESP_LOGI(TAG, "IO OTA complete — %lu bytes sent", (unsigned long)fw_size);
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool uart_ota_trigger(void)
|
||||||
|
{
|
||||||
|
if (!g_io_update.available) {
|
||||||
|
ESP_LOGW(TAG, "no IO update available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (g_uart_ota_state != UART_OTA_S_IDLE) {
|
||||||
|
ESP_LOGW(TAG, "UART OTA busy (state=%d)", g_uart_ota_state);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/* Init UART1 for OTA */
|
||||||
|
uart_config_t ucfg = {
|
||||||
|
.baud_rate = UART_OTA_BAUD,
|
||||||
|
.data_bits = UART_DATA_8_BITS,
|
||||||
|
.parity = UART_PARITY_DISABLE,
|
||||||
|
.stop_bits = UART_STOP_BITS_1,
|
||||||
|
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
||||||
|
};
|
||||||
|
uart_param_config(UART_OTA_PORT, &ucfg);
|
||||||
|
uart_set_pin(UART_OTA_PORT, UART_OTA_TX_GPIO, UART_OTA_RX_GPIO,
|
||||||
|
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
||||||
|
uart_driver_install(UART_OTA_PORT, 2048, 0, 0, NULL, 0);
|
||||||
|
|
||||||
|
xTaskCreate(uart_ota_task, "uart_ota", 16384, NULL, 4, NULL);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
64
esp32s3/balance/main/uart_ota.h
Normal file
64
esp32s3/balance/main/uart_ota.h
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
#pragma once
|
||||||
|
/* uart_ota.h — UART OTA protocol for Balance→IO firmware update (bd-21hv)
|
||||||
|
*
|
||||||
|
* Balance downloads io-firmware.bin from Gitea, then streams it to the IO
|
||||||
|
* board over UART1 (GPIO17/18, 460800 baud) in 1 KB chunks with ACK.
|
||||||
|
*
|
||||||
|
* Protocol frame format (both directions):
|
||||||
|
* [TYPE:1][SEQ:2 BE][LEN:2 BE][PAYLOAD:LEN][CRC8:1]
|
||||||
|
* CRC8-SMBUS over TYPE+SEQ+LEN+PAYLOAD.
|
||||||
|
*
|
||||||
|
* Balance→IO:
|
||||||
|
* OTA_BEGIN (0xC0) payload: uint32 total_size BE + uint8[32] sha256
|
||||||
|
* OTA_DATA (0xC1) payload: uint8[] chunk (up to 1024 bytes)
|
||||||
|
* OTA_END (0xC2) no payload
|
||||||
|
* OTA_ABORT (0xC3) no payload
|
||||||
|
*
|
||||||
|
* IO→Balance:
|
||||||
|
* OTA_ACK (0xC4) payload: uint16 acked_seq BE
|
||||||
|
* OTA_NACK (0xC5) payload: uint16 failed_seq BE + uint8 err_code
|
||||||
|
* OTA_STATUS (0xC6) payload: uint8 state + uint8 progress%
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/* UART for Balance→IO OTA */
|
||||||
|
#include "driver/uart.h"
|
||||||
|
#define UART_OTA_PORT UART_NUM_1
|
||||||
|
#define UART_OTA_BAUD 460800
|
||||||
|
#define UART_OTA_TX_GPIO 17
|
||||||
|
#define UART_OTA_RX_GPIO 18
|
||||||
|
|
||||||
|
#define OTA_UART_CHUNK_SIZE 1024
|
||||||
|
#define OTA_UART_ACK_TIMEOUT_MS 3000
|
||||||
|
#define OTA_UART_MAX_RETRIES 3
|
||||||
|
|
||||||
|
/* Frame type bytes */
|
||||||
|
#define UART_OTA_BEGIN 0xC0u
|
||||||
|
#define UART_OTA_DATA 0xC1u
|
||||||
|
#define UART_OTA_END 0xC2u
|
||||||
|
#define UART_OTA_ABORT 0xC3u
|
||||||
|
#define UART_OTA_ACK 0xC4u
|
||||||
|
#define UART_OTA_NACK 0xC5u
|
||||||
|
#define UART_OTA_STATUS 0xC6u
|
||||||
|
|
||||||
|
/* NACK error codes */
|
||||||
|
#define OTA_ERR_BAD_CRC 0x01u
|
||||||
|
#define OTA_ERR_WRITE 0x02u
|
||||||
|
#define OTA_ERR_SIZE 0x03u
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
UART_OTA_S_IDLE = 0,
|
||||||
|
UART_OTA_S_DOWNLOADING, /* downloading from Gitea */
|
||||||
|
UART_OTA_S_SENDING, /* sending to IO board */
|
||||||
|
UART_OTA_S_DONE,
|
||||||
|
UART_OTA_S_FAILED,
|
||||||
|
} uart_ota_send_state_t;
|
||||||
|
|
||||||
|
extern volatile uart_ota_send_state_t g_uart_ota_state;
|
||||||
|
extern volatile uint8_t g_uart_ota_progress;
|
||||||
|
|
||||||
|
/* Trigger IO firmware update. Uses g_io_update (from gitea_ota).
|
||||||
|
* Downloads bin, then streams via UART. Returns false if busy or no update. */
|
||||||
|
bool uart_ota_trigger(void);
|
||||||
14
esp32s3/balance/main/version.h
Normal file
14
esp32s3/balance/main/version.h
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
/* Embedded firmware version — bump on each release */
|
||||||
|
#define BALANCE_FW_VERSION "1.0.0"
|
||||||
|
#define IO_FW_VERSION "1.0.0"
|
||||||
|
|
||||||
|
/* Gitea release tag prefixes */
|
||||||
|
#define BALANCE_TAG_PREFIX "esp32-balance/"
|
||||||
|
#define IO_TAG_PREFIX "esp32-io/"
|
||||||
|
|
||||||
|
/* Gitea release asset filenames */
|
||||||
|
#define BALANCE_BIN_ASSET "balance-firmware.bin"
|
||||||
|
#define IO_BIN_ASSET "io-firmware.bin"
|
||||||
|
#define BALANCE_SHA256_ASSET "balance-firmware.sha256"
|
||||||
|
#define IO_SHA256_ASSET "io-firmware.sha256"
|
||||||
7
esp32s3/balance/partitions.csv
Normal file
7
esp32s3/balance/partitions.csv
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# ESP32-S3 BALANCE — 4 MB flash, dual OTA partitions
|
||||||
|
# Name, Type, SubType, Offset, Size
|
||||||
|
nvs, data, nvs, 0x9000, 0x5000,
|
||||||
|
otadata, data, ota, 0xe000, 0x2000,
|
||||||
|
app0, app, ota_0, 0x10000, 0x1B0000,
|
||||||
|
app1, app, ota_1, 0x1C0000, 0x1B0000,
|
||||||
|
nvs_user, data, nvs, 0x370000, 0x50000,
|
||||||
|
@ -9,3 +9,11 @@ CONFIG_ESP_CONSOLE_UART_DEFAULT=y
|
|||||||
CONFIG_ESP_CONSOLE_UART_NUM=0
|
CONFIG_ESP_CONSOLE_UART_NUM=0
|
||||||
CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200
|
CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200
|
||||||
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
||||||
|
|
||||||
|
# OTA — bd-3gwo: dual OTA partitions + rollback
|
||||||
|
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||||
|
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
||||||
|
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
|
||||||
|
CONFIG_OTA_ALLOW_HTTP=y
|
||||||
|
CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y
|
||||||
|
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
|
||||||
|
|||||||
3
esp32s3/io/CMakeLists.txt
Normal file
3
esp32s3/io/CMakeLists.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
project(esp32s3_io)
|
||||||
10
esp32s3/io/main/CMakeLists.txt
Normal file
10
esp32s3/io/main/CMakeLists.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
idf_component_register(
|
||||||
|
SRCS "main.c" "uart_ota_recv.c"
|
||||||
|
INCLUDE_DIRS "."
|
||||||
|
REQUIRES
|
||||||
|
app_update
|
||||||
|
mbedtls
|
||||||
|
driver
|
||||||
|
freertos
|
||||||
|
esp_timer
|
||||||
|
)
|
||||||
35
esp32s3/io/main/config.h
Normal file
35
esp32s3/io/main/config.h
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
/* ESP32-S3 IO board — pin assignments (SAUL-TEE-SYSTEM-REFERENCE.md) */
|
||||||
|
|
||||||
|
/* ── Inter-board UART (to/from BALANCE board) ── */
|
||||||
|
#define IO_UART_PORT UART_NUM_0
|
||||||
|
#define IO_UART_BAUD 460800
|
||||||
|
#define IO_UART_TX_GPIO 43 /* IO board UART0_TXD → BALANCE RX */
|
||||||
|
#define IO_UART_RX_GPIO 44 /* IO board UART0_RXD ← BALANCE TX */
|
||||||
|
/* Note: SAUL-TEE spec says IO TX=IO18, RX=IO21; BALANCE TX=IO17, RX=IO18.
|
||||||
|
* This is UART0 on the IO devkit (GPIO43/44). Adjust to match actual wiring. */
|
||||||
|
|
||||||
|
/* ── BTS7960 Left motor driver ── */
|
||||||
|
#define MOTOR_L_RPWM 1
|
||||||
|
#define MOTOR_L_LPWM 2
|
||||||
|
#define MOTOR_L_EN_R 3
|
||||||
|
#define MOTOR_L_EN_L 4
|
||||||
|
|
||||||
|
/* ── BTS7960 Right motor driver ── */
|
||||||
|
#define MOTOR_R_RPWM 5
|
||||||
|
#define MOTOR_R_LPWM 6
|
||||||
|
#define MOTOR_R_EN_R 7
|
||||||
|
#define MOTOR_R_EN_L 8
|
||||||
|
|
||||||
|
/* ── Arming button / kill switch ── */
|
||||||
|
#define ARM_BTN_GPIO 9
|
||||||
|
#define KILL_GPIO 10
|
||||||
|
|
||||||
|
/* ── WS2812B LED strip ── */
|
||||||
|
#define LED_DATA_GPIO 13
|
||||||
|
|
||||||
|
/* ── OTA UART — receives firmware from BALANCE (bd-21hv) ── */
|
||||||
|
/* Uses same IO_UART_PORT since Balance drives OTA over the inter-board link */
|
||||||
|
|
||||||
|
/* ── Firmware version ── */
|
||||||
|
#define IO_FW_VERSION "1.0.0"
|
||||||
42
esp32s3/io/main/main.c
Normal file
42
esp32s3/io/main/main.c
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/* main.c — ESP32-S3 IO board app_main */
|
||||||
|
|
||||||
|
#include "uart_ota_recv.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_ota_ops.h"
|
||||||
|
#include "driver/uart.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
|
||||||
|
static const char *TAG = "io_main";
|
||||||
|
|
||||||
|
static void uart_init(void)
|
||||||
|
{
|
||||||
|
uart_config_t cfg = {
|
||||||
|
.baud_rate = IO_UART_BAUD,
|
||||||
|
.data_bits = UART_DATA_8_BITS,
|
||||||
|
.parity = UART_PARITY_DISABLE,
|
||||||
|
.stop_bits = UART_STOP_BITS_1,
|
||||||
|
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
||||||
|
};
|
||||||
|
uart_param_config(IO_UART_PORT, &cfg);
|
||||||
|
uart_set_pin(IO_UART_PORT, IO_UART_TX_GPIO, IO_UART_RX_GPIO,
|
||||||
|
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
||||||
|
uart_driver_install(IO_UART_PORT, 4096, 0, 0, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void app_main(void)
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "ESP32-S3 IO v%s starting", IO_FW_VERSION);
|
||||||
|
|
||||||
|
/* Mark running image valid (OTA rollback support) */
|
||||||
|
esp_ota_mark_app_valid_cancel_rollback();
|
||||||
|
|
||||||
|
uart_init();
|
||||||
|
uart_ota_recv_init();
|
||||||
|
|
||||||
|
/* IO board main loop placeholder — RC/motor/sensor tasks added in later beads */
|
||||||
|
while (1) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
210
esp32s3/io/main/uart_ota_recv.c
Normal file
210
esp32s3/io/main/uart_ota_recv.c
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
/* uart_ota_recv.c — IO board OTA receiver (bd-21hv)
|
||||||
|
*
|
||||||
|
* Listens on UART0 for OTA frames from Balance board.
|
||||||
|
* Writes incoming chunks to the inactive OTA partition, verifies SHA256,
|
||||||
|
* then reboots into new firmware.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uart_ota_recv.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_ota_ops.h"
|
||||||
|
#include "driver/uart.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "mbedtls/sha256.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static const char *TAG = "io_ota";
|
||||||
|
|
||||||
|
volatile io_ota_state_t g_io_ota_state = IO_OTA_IDLE;
|
||||||
|
volatile uint8_t g_io_ota_progress = 0;
|
||||||
|
|
||||||
|
/* Frame type bytes (same as uart_ota.h sender side) */
|
||||||
|
#define OTA_BEGIN 0xC0u
|
||||||
|
#define OTA_DATA 0xC1u
|
||||||
|
#define OTA_END 0xC2u
|
||||||
|
#define OTA_ABORT 0xC3u
|
||||||
|
#define OTA_ACK 0xC4u
|
||||||
|
#define OTA_NACK 0xC5u
|
||||||
|
|
||||||
|
#define CHUNK_MAX 1024
|
||||||
|
|
||||||
|
static uint8_t crc8(const uint8_t *d, uint16_t len)
|
||||||
|
{
|
||||||
|
uint8_t crc = 0;
|
||||||
|
for (uint16_t i = 0; i < len; i++) {
|
||||||
|
crc ^= d[i];
|
||||||
|
for (uint8_t b = 0; b < 8; b++)
|
||||||
|
crc = (crc & 0x80u) ? (uint8_t)((crc << 1u) ^ 0x07u) : (uint8_t)(crc << 1u);
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_ack(uint16_t seq)
|
||||||
|
{
|
||||||
|
uint8_t frame[6];
|
||||||
|
frame[0] = OTA_ACK;
|
||||||
|
frame[1] = (uint8_t)(seq >> 8u);
|
||||||
|
frame[2] = (uint8_t)(seq);
|
||||||
|
frame[3] = 0; frame[4] = 0; /* LEN=0 */
|
||||||
|
uint8_t crc = crc8(frame, 5);
|
||||||
|
frame[5] = crc;
|
||||||
|
uart_write_bytes(IO_UART_PORT, (char *)frame, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_nack(uint16_t seq, uint8_t err)
|
||||||
|
{
|
||||||
|
uint8_t frame[8];
|
||||||
|
frame[0] = OTA_NACK;
|
||||||
|
frame[1] = (uint8_t)(seq >> 8u);
|
||||||
|
frame[2] = (uint8_t)(seq);
|
||||||
|
frame[3] = 0; frame[4] = 1; /* LEN=1 */
|
||||||
|
frame[5] = err;
|
||||||
|
uint8_t crc = crc8(frame, 6);
|
||||||
|
frame[6] = crc;
|
||||||
|
uart_write_bytes(IO_UART_PORT, (char *)frame, 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read exact n bytes with timeout */
|
||||||
|
static bool uart_read_exact(uint8_t *buf, int n, int timeout_ms)
|
||||||
|
{
|
||||||
|
int got = 0;
|
||||||
|
while (got < n && timeout_ms > 0) {
|
||||||
|
int r = uart_read_bytes(IO_UART_PORT, buf + got, n - got,
|
||||||
|
pdMS_TO_TICKS(50));
|
||||||
|
if (r > 0) got += r;
|
||||||
|
else timeout_ms -= 50;
|
||||||
|
}
|
||||||
|
return got == n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ota_recv_task(void *arg)
|
||||||
|
{
|
||||||
|
esp_ota_handle_t handle = 0;
|
||||||
|
const esp_partition_t *ota_part = esp_ota_get_next_update_partition(NULL);
|
||||||
|
mbedtls_sha256_context sha;
|
||||||
|
mbedtls_sha256_init(&sha);
|
||||||
|
uint32_t expected_size = 0;
|
||||||
|
uint8_t expected_digest[32] = {0};
|
||||||
|
uint32_t received = 0;
|
||||||
|
bool ota_started = false;
|
||||||
|
static uint8_t payload[CHUNK_MAX];
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
/* Read frame header: TYPE(1) + SEQ(2) + LEN(2) = 5 bytes */
|
||||||
|
uint8_t hdr[5];
|
||||||
|
if (!uart_read_exact(hdr, 5, 5000)) continue;
|
||||||
|
|
||||||
|
uint8_t type = hdr[0];
|
||||||
|
uint16_t seq = (uint16_t)((hdr[1] << 8u) | hdr[2]);
|
||||||
|
uint16_t plen = (uint16_t)((hdr[3] << 8u) | hdr[4]);
|
||||||
|
|
||||||
|
if (plen > CHUNK_MAX + 36) {
|
||||||
|
ESP_LOGW(TAG, "oversized frame plen=%u", plen);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read payload + CRC */
|
||||||
|
if (plen > 0 && !uart_read_exact(payload, plen, 2000)) continue;
|
||||||
|
uint8_t crc_rx;
|
||||||
|
if (!uart_read_exact(&crc_rx, 1, 500)) continue;
|
||||||
|
|
||||||
|
/* Verify CRC over hdr+payload */
|
||||||
|
uint8_t crc_buf[5 + CHUNK_MAX + 36];
|
||||||
|
memcpy(crc_buf, hdr, 5);
|
||||||
|
if (plen > 0) memcpy(crc_buf + 5, payload, plen);
|
||||||
|
uint8_t expected_crc = crc8(crc_buf, (uint16_t)(5 + plen));
|
||||||
|
if (crc_rx != expected_crc) {
|
||||||
|
ESP_LOGW(TAG, "CRC fail seq=%u", seq);
|
||||||
|
send_nack(seq, 0x01u); /* OTA_ERR_BAD_CRC */
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case OTA_BEGIN:
|
||||||
|
if (plen < 36) { send_nack(seq, 0x03u); break; }
|
||||||
|
expected_size = ((uint32_t)payload[0] << 24u) |
|
||||||
|
((uint32_t)payload[1] << 16u) |
|
||||||
|
((uint32_t)payload[2] << 8u) |
|
||||||
|
(uint32_t)payload[3];
|
||||||
|
memcpy(expected_digest, &payload[4], 32);
|
||||||
|
|
||||||
|
if (!ota_part || esp_ota_begin(ota_part, OTA_WITH_SEQUENTIAL_WRITES,
|
||||||
|
&handle) != ESP_OK) {
|
||||||
|
send_nack(seq, 0x02u);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mbedtls_sha256_starts(&sha, 0);
|
||||||
|
received = 0;
|
||||||
|
ota_started = true;
|
||||||
|
g_io_ota_state = IO_OTA_RECEIVING;
|
||||||
|
g_io_ota_progress = 0;
|
||||||
|
ESP_LOGI(TAG, "OTA begin: %lu bytes", (unsigned long)expected_size);
|
||||||
|
send_ack(seq);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OTA_DATA:
|
||||||
|
if (!ota_started) { send_nack(seq, 0x02u); break; }
|
||||||
|
if (esp_ota_write(handle, payload, plen) != ESP_OK) {
|
||||||
|
send_nack(seq, 0x02u);
|
||||||
|
esp_ota_abort(handle);
|
||||||
|
ota_started = false;
|
||||||
|
g_io_ota_state = IO_OTA_FAILED;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mbedtls_sha256_update(&sha, payload, plen);
|
||||||
|
received += plen;
|
||||||
|
if (expected_size > 0)
|
||||||
|
g_io_ota_progress = (uint8_t)((received * 100u) / expected_size);
|
||||||
|
send_ack(seq);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OTA_END: {
|
||||||
|
if (!ota_started) { send_nack(seq, 0x02u); break; }
|
||||||
|
g_io_ota_state = IO_OTA_VERIFYING;
|
||||||
|
|
||||||
|
uint8_t digest[32];
|
||||||
|
mbedtls_sha256_finish(&sha, digest);
|
||||||
|
if (memcmp(digest, expected_digest, 32) != 0) {
|
||||||
|
ESP_LOGE(TAG, "SHA256 mismatch");
|
||||||
|
esp_ota_abort(handle);
|
||||||
|
send_nack(seq, 0x01u);
|
||||||
|
g_io_ota_state = IO_OTA_FAILED;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (esp_ota_end(handle) != ESP_OK ||
|
||||||
|
esp_ota_set_boot_partition(ota_part) != ESP_OK) {
|
||||||
|
send_nack(seq, 0x02u);
|
||||||
|
g_io_ota_state = IO_OTA_FAILED;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_io_ota_state = IO_OTA_REBOOTING;
|
||||||
|
g_io_ota_progress = 100;
|
||||||
|
ESP_LOGI(TAG, "OTA done — rebooting");
|
||||||
|
send_ack(seq);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
|
esp_restart();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case OTA_ABORT:
|
||||||
|
if (ota_started) { esp_ota_abort(handle); ota_started = false; }
|
||||||
|
g_io_ota_state = IO_OTA_IDLE;
|
||||||
|
ESP_LOGW(TAG, "OTA aborted");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void uart_ota_recv_init(void)
|
||||||
|
{
|
||||||
|
/* UART0 already initialized for inter-board comms; just create the task */
|
||||||
|
xTaskCreate(ota_recv_task, "io_ota_recv", 8192, NULL, 6, NULL);
|
||||||
|
ESP_LOGI(TAG, "OTA receiver task started");
|
||||||
|
}
|
||||||
20
esp32s3/io/main/uart_ota_recv.h
Normal file
20
esp32s3/io/main/uart_ota_recv.h
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
/* uart_ota_recv.h — IO board: receives OTA firmware from Balance (bd-21hv) */
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
IO_OTA_IDLE = 0,
|
||||||
|
IO_OTA_RECEIVING,
|
||||||
|
IO_OTA_VERIFYING,
|
||||||
|
IO_OTA_APPLYING,
|
||||||
|
IO_OTA_REBOOTING,
|
||||||
|
IO_OTA_FAILED,
|
||||||
|
} io_ota_state_t;
|
||||||
|
|
||||||
|
extern volatile io_ota_state_t g_io_ota_state;
|
||||||
|
extern volatile uint8_t g_io_ota_progress;
|
||||||
|
|
||||||
|
/* Start listening for OTA frames on UART0 */
|
||||||
|
void uart_ota_recv_init(void);
|
||||||
7
esp32s3/io/partitions.csv
Normal file
7
esp32s3/io/partitions.csv
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# ESP32-S3 IO — 4 MB flash, dual OTA partitions
|
||||||
|
# Name, Type, SubType, Offset, Size
|
||||||
|
nvs, data, nvs, 0x9000, 0x5000,
|
||||||
|
otadata, data, ota, 0xe000, 0x2000,
|
||||||
|
app0, app, ota_0, 0x10000, 0x1B0000,
|
||||||
|
app1, app, ota_1, 0x1C0000, 0x1B0000,
|
||||||
|
nvs_user, data, nvs, 0x370000, 0x50000,
|
||||||
|
13
esp32s3/io/sdkconfig.defaults
Normal file
13
esp32s3/io/sdkconfig.defaults
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
CONFIG_IDF_TARGET="esp32s3"
|
||||||
|
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||||
|
CONFIG_FREERTOS_HZ=1000
|
||||||
|
CONFIG_ESP_TASK_WDT_EN=y
|
||||||
|
CONFIG_ESP_TASK_WDT_TIMEOUT_S=5
|
||||||
|
CONFIG_UART_ISR_IN_IRAM=y
|
||||||
|
CONFIG_ESP_CONSOLE_UART_DEFAULT=y
|
||||||
|
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
||||||
|
|
||||||
|
# OTA — bd-3gwo: dual OTA partitions + rollback
|
||||||
|
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||||
|
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
||||||
|
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
|
||||||
Loading…
x
Reference in New Issue
Block a user