sl-firmware 7ef6220e15 feat: UART OTA protocol Balance→IO board, 1 KB chunk + ACK (bd-21hv)
Balance side (uart_ota.c): downloads io-firmware.bin from Gitea to RAM,
computes SHA256, then streams to IO over UART1 (GPIO17/18, 460800 baud)
as OTA_BEGIN/OTA_DATA/OTA_END frames with CRC8 + per-chunk ACK/retry (×3).
IO side (uart_ota_recv.c): receives frames, writes to inactive OTA partition
via esp_ota_write, verifies SHA256 on OTA_END, sets boot partition, reboots.
IO board main.c + CMakeLists.txt scaffold included.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 22:16:11 -04:00

242 lines
7.4 KiB
C

/* 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;
}