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>
242 lines
7.4 KiB
C
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;
|
|
}
|