- UART binary mode switch mechanism: enter handshake marker '[ota] binary mode ON', explicit exit via OTA_ABORT (immediate), 10s idle fallback (was 3s, must exceed the host retry window); host resync by CR probe - fix: otaIdleMs not re-armed on mode entry - every second 'ota' entry was kicked out by an instant idle timeout (leftover count from prior session) - lossy-link tolerance: device drops partial frames after a 100ms byte gap so host resends re-align; OTA session is now single-owner (second BEGIN on another channel gets BAD_STATE) - ble_ota_update.py: wait for the enter marker instead of blind sleeps, per-frame resend (2s x3), BAD_STATE+expected-offset treated as implicit ack for duplicate DATA/BEGIN (resync after ack loss), stage-labeled timeouts with raw-rx dump - verified on hardware: UART OTA full pass (55.7KB/41s, 4-5 lost frames auto-recovered) + BLE OTA regression pass; docs: dev log 44, ble_protocol.md 6.6 rework, AGENTS.md sync
695 lines
22 KiB
C
695 lines
22 KiB
C
/**
|
|
* @file app_ota.c
|
|
* @brief OTA receiver implementation (channel-agnostic: BLE OTA
|
|
* characteristic, UART binary mode, legacy notify frame path).
|
|
*
|
|
* Flow: OTA_BEGIN (size/crc/version) -> OTA_DATA* (strictly sequential
|
|
* offsets, EVERY frame acked with the new offset - lockstep flow control)
|
|
* -> OTA_END (flush the sub-word tail, verify whole-image CRC32 against
|
|
* flash, verify the vector table targets the right bank, update
|
|
* bootsetting, reset). OTA_ABORT aborts the session.
|
|
*
|
|
* Direct-write design (no 4KB staging buffer):
|
|
* - Sectors of the target bank are erased lazily: the first received byte
|
|
* falling into a not-yet-erased sector triggers that sector's erase.
|
|
* - Received payload bytes are programmed to flash immediately; only a
|
|
* 4-byte word staging buffer is kept because Qflash_Write() requires a
|
|
* 4-byte aligned length (tail bytes are padded with 0xFF at OTA_END).
|
|
*
|
|
* Notes:
|
|
* - Runs in the BLE schedule task context; vTaskDelay() is available.
|
|
* - Qflash erase/write disable interrupts for tens of ms per sector (SDK
|
|
* driver behavior, the flash algorithm executes from RAM). The BLE link
|
|
* survives this via the 5 s connection supervision timeout.
|
|
*/
|
|
#include "app_ota.h"
|
|
#include "app_ble_proto.h"
|
|
#include "bsp_usart.h"
|
|
#include "dfu_layout.h"
|
|
#include "boot_crc32.h"
|
|
#include "n32wb03x.h"
|
|
#include "n32wb03x_qflash.h"
|
|
|
|
#include <string.h>
|
|
#include <stddef.h>
|
|
|
|
#include "FreeRTOS.h"
|
|
#include "task.h"
|
|
|
|
/* armlink execution-region base symbol: address of this running image */
|
|
extern uint32_t Image$$ER_IROM1$$Base;
|
|
|
|
#define OTA_MIN_IMAGE_SIZE 4u
|
|
/* Deferred reset after a successful OTA_END: the END response must reach
|
|
* the host before the chip resets, otherwise the ack is lost (BLE: the RSP
|
|
* sits in the read-back buffer until the host's ATT read fetches it).
|
|
* Reset once the response was consumed AND a short grace elapsed (lets the
|
|
* controller actually emit the ATT read response), or unconditionally after
|
|
* the timeout so the device never hangs. */
|
|
#define OTA_RESET_GRACE_MS 300u
|
|
#define OTA_RESET_TIMEOUT_MS 2000u
|
|
|
|
/* OTA session state */
|
|
static uint8_t s_active; /* session in progress */
|
|
static uint32_t s_target_base; /* opposite bank base */
|
|
static uint32_t s_total_size;
|
|
static uint32_t s_image_crc32;
|
|
static uint32_t s_version;
|
|
static uint32_t s_offset; /* bytes received so far */
|
|
static uint32_t s_prog; /* bytes programmed to flash */
|
|
static uint32_t s_erased; /* bytes erased (sector granularity) */
|
|
static uint8_t s_word_buf[4]; /* sub-word alignment staging */
|
|
static uint32_t s_word_fill;
|
|
static uint8_t s_qflash_ready;
|
|
|
|
/* Deferred post-OTA_END reset (see OTA_RESET_GRACE_MS above) */
|
|
static volatile uint8_t s_rsp_consumed; /* END RSP taken by the host */
|
|
static volatile uint8_t s_reset_pending; /* reset scheduled, polled by the BLE task */
|
|
static TickType_t s_reset_stamp;
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Helpers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static uint32_t ota_rd32(const uint8_t* p)
|
|
{
|
|
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
|
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Channel-agnostic response routing */
|
|
/* */
|
|
/* The OTA engine speaks 0xCA-framed OTA_RSP messages. Each transport */
|
|
/* channel (legacy notify, BLE OTA characteristic, UART binary mode) */
|
|
/* registers a sink before feeding frames in; the sink receives one */
|
|
/* complete framed OTA_RSP. NULL sink = legacy notify path via */
|
|
/* app_ble_proto_send_frame(). */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static app_ota_rsp_sink_fn s_rsp_sink;
|
|
|
|
/**
|
|
* @brief CRC16-CCITT (poly 0x1021, init 0xFFFF) - same as the frame
|
|
* protocol's proto_crc16 (app_ble_proto.c).
|
|
*/
|
|
static uint16_t ota_crc16(const uint8_t* d, uint16_t len)
|
|
{
|
|
uint16_t crc = 0xFFFFu;
|
|
|
|
while (len--)
|
|
{
|
|
uint8_t bit;
|
|
crc ^= (uint16_t)*d++ << 8;
|
|
for (bit = 0; bit < 8u; bit++)
|
|
{
|
|
crc = (crc & 0x8000u) ? (uint16_t)((crc << 1) ^ 0x1021u)
|
|
: (uint16_t)(crc << 1);
|
|
}
|
|
}
|
|
return crc;
|
|
}
|
|
|
|
/**
|
|
* @brief Send an OTA_RSP frame: {cmd_echo, status, offset(LE)}.
|
|
*/
|
|
static void ota_respond(uint8_t seq, uint8_t cmd_echo, uint8_t status, uint32_t offset)
|
|
{
|
|
uint8_t p[6];
|
|
|
|
p[0] = cmd_echo;
|
|
p[1] = status;
|
|
p[2] = (uint8_t)(offset & 0xFFu);
|
|
p[3] = (uint8_t)((offset >> 8) & 0xFFu);
|
|
p[4] = (uint8_t)((offset >> 16) & 0xFFu);
|
|
p[5] = (uint8_t)((offset >> 24) & 0xFFu);
|
|
|
|
if (s_rsp_sink != NULL)
|
|
{
|
|
/* Wrap into a 0xCA frame and hand to the channel */
|
|
uint8_t fr[5 + sizeof(p) + 2];
|
|
uint16_t crc;
|
|
|
|
fr[0] = BLE_FRAME_SOF;
|
|
fr[1] = BLE_FRAME_OTA_RSP;
|
|
fr[2] = seq;
|
|
fr[3] = sizeof(p) & 0xFFu;
|
|
fr[4] = 0;
|
|
memcpy(fr + 5, p, sizeof(p));
|
|
crc = ota_crc16(fr + 1, (uint16_t)(4u + sizeof(p))); /* TYPE..PAYLOAD */
|
|
fr[5 + sizeof(p)] = (uint8_t)(crc & 0xFFu);
|
|
fr[5 + sizeof(p) + 1] = (uint8_t)(crc >> 8);
|
|
s_rsp_sink(fr, (uint16_t)sizeof(fr));
|
|
return;
|
|
}
|
|
(void)app_ble_proto_send_frame(BLE_FRAME_OTA_RSP, seq, p, sizeof(p));
|
|
}
|
|
|
|
uint32_t app_ota_current_bank_base(void)
|
|
{
|
|
return (uint32_t)&Image$$ER_IROM1$$Base;
|
|
}
|
|
|
|
void app_ota_abort(void)
|
|
{
|
|
s_active = 0;
|
|
s_offset = 0;
|
|
s_prog = 0;
|
|
s_erased = 0;
|
|
s_word_fill = 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Erase every sector of the target bank that [s_erased, end) touches.
|
|
* @return 0 on success, BLE_OTA_ST_FLASH_FAIL on error.
|
|
*/
|
|
static uint8_t ota_erase_up_to(uint32_t end)
|
|
{
|
|
while (s_erased < end)
|
|
{
|
|
if (Qflash_Erase_Sector(s_target_base + s_erased) != 0)
|
|
{
|
|
return BLE_OTA_ST_FLASH_FAIL;
|
|
}
|
|
s_erased += CAIIC_FLASH_SECTOR_SIZE;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Program one chunk to flash at the current program pointer.
|
|
* @return 0 on success, BLE_OTA_ST_FLASH_FAIL on error.
|
|
*/
|
|
static uint8_t ota_program(const uint8_t* data, uint32_t len)
|
|
{
|
|
if (Qflash_Write(s_target_base + s_prog, (uint8_t*)data, len) != 0)
|
|
{
|
|
return BLE_OTA_ST_FLASH_FAIL;
|
|
}
|
|
s_prog += len;
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Frame handlers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/**
|
|
* @brief OTA_BEGIN: {total_size u32, image_crc32 u32, version u32}.
|
|
*/
|
|
static void ota_on_begin(uint8_t seq, const uint8_t* p, uint16_t len)
|
|
{
|
|
uint32_t total, crc, version;
|
|
uint32_t self, target;
|
|
|
|
if (len != 12u)
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_BAD_FRAME, 0);
|
|
return;
|
|
}
|
|
|
|
total = ota_rd32(p);
|
|
crc = ota_rd32(p + 4);
|
|
version = ota_rd32(p + 8);
|
|
|
|
/* One session at a time across all channels: a second BEGIN (e.g. BLE
|
|
* while a UART session runs) is refused instead of corrupting state */
|
|
if (s_active)
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_BAD_STATE, s_offset);
|
|
return;
|
|
}
|
|
|
|
/* Pick the opposite bank as the update target */
|
|
self = app_ota_current_bank_base();
|
|
if (self == CAIIC_APP1_BASE)
|
|
{
|
|
target = CAIIC_APP2_BASE;
|
|
}
|
|
else if (self == CAIIC_APP2_BASE)
|
|
{
|
|
target = CAIIC_APP1_BASE;
|
|
}
|
|
else
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_BAD_STATE, 0);
|
|
return;
|
|
}
|
|
|
|
if (total < OTA_MIN_IMAGE_SIZE || total > CAIIC_APP_BANK_SIZE)
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_SIZE_TOO_BIG, 0);
|
|
return;
|
|
}
|
|
|
|
if (!s_qflash_ready)
|
|
{
|
|
Qflash_Init(); /* copy the flash algorithm to RAM (once) */
|
|
s_qflash_ready = 1;
|
|
}
|
|
|
|
s_active = 1;
|
|
s_target_base = target;
|
|
s_total_size = total;
|
|
s_image_crc32 = crc;
|
|
s_version = version;
|
|
s_offset = 0;
|
|
s_prog = 0;
|
|
s_erased = 0;
|
|
s_word_fill = 0;
|
|
|
|
ota_respond(seq, BLE_FRAME_OTA_BEGIN, BLE_OTA_ST_OK, 0);
|
|
}
|
|
|
|
/**
|
|
* @brief OTA_DATA: {offset u32 + data}. Data must arrive strictly in order.
|
|
* Payload is programmed to flash immediately (sectors are erased
|
|
* lazily); EVERY frame is acked with the new offset (lockstep flow
|
|
* control on the BLE/UART OTA channels; the erase time of a fresh
|
|
* sector is covered by the ack round trip).
|
|
* Errors are acked immediately and the state is
|
|
* kept so the peer can resend.
|
|
*/
|
|
static void ota_on_data(uint8_t seq, const uint8_t* p, uint16_t len)
|
|
{
|
|
uint32_t off;
|
|
const uint8_t* data;
|
|
uint32_t dlen;
|
|
uint8_t err;
|
|
|
|
if (!s_active)
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_BAD_STATE, 0);
|
|
return;
|
|
}
|
|
if (len < 4u)
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_BAD_FRAME, s_offset);
|
|
return;
|
|
}
|
|
|
|
off = ota_rd32(p);
|
|
data = p + 4;
|
|
dlen = (uint32_t)len - 4u;
|
|
|
|
if (off != s_offset)
|
|
{
|
|
/* out-of-order: report the expected offset, keep the session */
|
|
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_BAD_STATE, s_offset);
|
|
return;
|
|
}
|
|
if (s_offset + dlen > s_total_size)
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_SIZE_TOO_BIG, s_offset);
|
|
return;
|
|
}
|
|
s_offset += dlen;
|
|
|
|
/* Lazily erase every sector this chunk touches */
|
|
err = ota_erase_up_to(s_offset);
|
|
|
|
if (err == 0 && s_word_fill != 0)
|
|
{
|
|
/* Complete the staged partial word first */
|
|
while (s_word_fill < 4u && dlen != 0)
|
|
{
|
|
s_word_buf[s_word_fill++] = *data++;
|
|
dlen--;
|
|
}
|
|
if (s_word_fill == 4u)
|
|
{
|
|
err = ota_program(s_word_buf, 4u);
|
|
s_word_fill = 0;
|
|
}
|
|
}
|
|
if (err == 0 && dlen >= 4u)
|
|
{
|
|
/* Bulk aligned part goes straight from the frame payload to flash */
|
|
uint32_t n4 = dlen & ~3u;
|
|
err = ota_program(data, n4);
|
|
data += n4;
|
|
dlen -= n4;
|
|
}
|
|
if (err == 0 && dlen != 0)
|
|
{
|
|
/* Sub-word tail: stage until the next chunk or OTA_END */
|
|
memcpy(s_word_buf, data, dlen);
|
|
s_word_fill = dlen;
|
|
}
|
|
if (err != 0)
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_DATA, err, s_offset);
|
|
app_ota_abort();
|
|
return;
|
|
}
|
|
/* Lockstep ack: every DATA frame is answered with the new offset */
|
|
ota_respond(seq, BLE_FRAME_OTA_DATA, BLE_OTA_ST_OK, s_offset);
|
|
}
|
|
|
|
/**
|
|
* @brief Program the bootsetting record and verify by read-back.
|
|
* @return 0 on success, BLE_OTA_ST_FLASH_FAIL on error.
|
|
*/
|
|
static uint8_t ota_update_bootsetting(void)
|
|
{
|
|
caiic_bootsetting_t bs;
|
|
caiic_bank_t* bank;
|
|
|
|
memcpy(&bs, (const void*)CAIIC_BOOTSETTING_ADDR, sizeof(bs));
|
|
|
|
/* Validate the existing record; rebuild from scratch when invalid */
|
|
if (bs.magic != CAIIC_BOOT_MAGIC ||
|
|
bs.crc32 != caiic_crc32((const uint8_t*)CAIIC_BOOTSETTING_ADDR,
|
|
(uint32_t)offsetof(caiic_bootsetting_t, crc32)))
|
|
{
|
|
memset(&bs, 0, sizeof(bs));
|
|
}
|
|
|
|
bs.magic = CAIIC_BOOT_MAGIC;
|
|
bs.active_bank = (s_target_base == CAIIC_APP1_BASE) ? CAIIC_ACTIVE_BANK1
|
|
: CAIIC_ACTIVE_BANK2;
|
|
bank = (s_target_base == CAIIC_APP1_BASE) ? &bs.bank1 : &bs.bank2;
|
|
bank->start_address = s_target_base;
|
|
bank->size = s_total_size;
|
|
bank->crc32 = s_image_crc32;
|
|
bank->version = s_version;
|
|
|
|
bs.crc32 = caiic_crc32((const uint8_t*)&bs, (uint32_t)offsetof(caiic_bootsetting_t, crc32));
|
|
|
|
if (Qflash_Erase_Sector(CAIIC_BOOTSETTING_ADDR) != 0)
|
|
{
|
|
return BLE_OTA_ST_FLASH_FAIL;
|
|
}
|
|
/* sizeof(caiic_bootsetting_t) = 44, already 4-byte aligned */
|
|
if (Qflash_Write(CAIIC_BOOTSETTING_ADDR, (uint8_t*)&bs, sizeof(bs)) != 0)
|
|
{
|
|
return BLE_OTA_ST_FLASH_FAIL;
|
|
}
|
|
if (memcmp(&bs, (const void*)CAIIC_BOOTSETTING_ADDR, sizeof(bs)) != 0)
|
|
{
|
|
return BLE_OTA_ST_FLASH_FAIL;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief OTA_END: {crc32 u32}. Flush the staged tail, verify, switch bank,
|
|
* reset.
|
|
*/
|
|
static void ota_on_end(uint8_t seq, const uint8_t* p, uint16_t len)
|
|
{
|
|
uint32_t crc_end, crc_calc;
|
|
|
|
if (!s_active)
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_BAD_STATE, 0);
|
|
return;
|
|
}
|
|
if (len != 4u)
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_BAD_FRAME, s_offset);
|
|
return;
|
|
}
|
|
crc_end = ota_rd32(p);
|
|
|
|
if (s_offset != s_total_size)
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_BAD_STATE, s_offset);
|
|
return;
|
|
}
|
|
|
|
/* Flush the staged sub-word tail, padded with 0xFF */
|
|
if (s_word_fill != 0)
|
|
{
|
|
while (s_word_fill < 4u)
|
|
{
|
|
s_word_buf[s_word_fill++] = 0xFFu;
|
|
}
|
|
if (ota_program(s_word_buf, 4u) != 0)
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_FLASH_FAIL, s_offset);
|
|
app_ota_abort();
|
|
return;
|
|
}
|
|
s_word_fill = 0;
|
|
}
|
|
|
|
/* Whole-image CRC32 read back from flash */
|
|
crc_calc = caiic_crc32((const uint8_t*)s_target_base, s_total_size);
|
|
if (crc_calc != crc_end || crc_calc != s_image_crc32)
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_CRC_FAIL, s_offset);
|
|
app_ota_abort();
|
|
return;
|
|
}
|
|
|
|
/* Link-address sanity: the image vector table's reset handler must land
|
|
* inside the target bank, otherwise the payload was linked for the wrong
|
|
* bank (code is position-dependent on Cortex-M0) - refuse to switch. */
|
|
{
|
|
uint32_t reset_pc = ota_rd32((const uint8_t*)s_target_base + 4u);
|
|
if (reset_pc < s_target_base + 8u ||
|
|
reset_pc >= s_target_base + s_total_size)
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_BANK_MISMATCH, s_offset);
|
|
app_ota_abort();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (ota_update_bootsetting() != 0)
|
|
{
|
|
ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_FLASH_FAIL, s_offset);
|
|
app_ota_abort();
|
|
return;
|
|
}
|
|
|
|
s_rsp_consumed = 0;
|
|
ota_respond(seq, BLE_FRAME_OTA_END, BLE_OTA_ST_OK, s_total_size);
|
|
|
|
/* Defer the reset until the host has fetched the END response (polled
|
|
* from the BLE schedule task, which keeps running meanwhile so the ATT
|
|
* read / notify flush can actually happen) */
|
|
s_reset_stamp = xTaskGetTickCount();
|
|
s_reset_pending = 1;
|
|
}
|
|
|
|
/**
|
|
* @brief Poll the deferred post-OTA_END reset. Must be called from the BLE
|
|
* schedule task loop (it keeps scheduling BLE events while the host
|
|
* fetches the END response).
|
|
*/
|
|
void app_ota_reset_poll(void)
|
|
{
|
|
if (s_reset_pending)
|
|
{
|
|
TickType_t elapsed = xTaskGetTickCount() - s_reset_stamp;
|
|
|
|
if ((s_rsp_consumed && elapsed >= pdMS_TO_TICKS(OTA_RESET_GRACE_MS)) ||
|
|
elapsed >= pdMS_TO_TICKS(OTA_RESET_TIMEOUT_MS))
|
|
{
|
|
NVIC_SystemReset();
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ota_uart_sink(const uint8_t* frame, uint16_t len);
|
|
|
|
static uint8_t s_uart_exit_req; /* OTA_ABORT on UART: drop back to CLI now */
|
|
|
|
void app_ota_handle_frame(uint8_t type, uint8_t seq, const uint8_t* payload, uint16_t len)
|
|
{
|
|
switch (type)
|
|
{
|
|
case BLE_FRAME_OTA_BEGIN:
|
|
ota_on_begin(seq, payload, len);
|
|
break;
|
|
case BLE_FRAME_OTA_DATA:
|
|
ota_on_data(seq, payload, len);
|
|
break;
|
|
case BLE_FRAME_OTA_END:
|
|
ota_on_end(seq, payload, len);
|
|
break;
|
|
case BLE_FRAME_OTA_ABORT:
|
|
app_ota_abort();
|
|
ota_respond(seq, BLE_FRAME_OTA_ABORT, BLE_OTA_ST_OK, s_offset);
|
|
/* Explicit exit from UART binary mode (ack already on the wire):
|
|
* the CLI frontend returns to text mode immediately instead of
|
|
* waiting out the 3 s idle timeout */
|
|
if (s_rsp_sink == ota_uart_sink)
|
|
{
|
|
s_uart_exit_req = 1;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Transport channels (docs/ble_protocol.md section 6.6) */
|
|
/* */
|
|
/* The OTA engine is channel-agnostic. Each channel feeds raw bytes */
|
|
/* through the reassembler below and gets OTA_RSP frames via its sink: */
|
|
/* - BLE OTA characteristic (...e0005): write-with-response carries */
|
|
/* one frame; the RSP is fetched by an ATT read (lockstep, no */
|
|
/* notify needed). */
|
|
/* - UART binary mode (CLI "ota"): frames on the raw byte stream, */
|
|
/* RSP frames go straight out on TX. */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
#define OTA_CHAN_MAX_PAYLOAD 480u /* same limit as the frame protocol */
|
|
|
|
enum
|
|
{
|
|
CHAN_RX_WAIT_SOF = 0,
|
|
CHAN_RX_HEADER,
|
|
CHAN_RX_BODY,
|
|
};
|
|
|
|
static uint8_t s_chanBuf[BLE_FRAME_HDR_LEN + OTA_CHAN_MAX_PAYLOAD + 2u];
|
|
static uint16_t s_chanHave;
|
|
static uint16_t s_chanNeed;
|
|
static uint8_t s_chanState;
|
|
|
|
/**
|
|
* @brief Feed channel bytes into the OTA frame reassembler.
|
|
* @param sink response sink for this channel (NULL = legacy notify path)
|
|
*/
|
|
void app_ota_chan_rx(const uint8_t* data, uint16_t len, app_ota_rsp_sink_fn sink)
|
|
{
|
|
uint16_t i;
|
|
|
|
s_rsp_sink = sink;
|
|
for (i = 0; i < len; i++)
|
|
{
|
|
uint8_t ch = data[i];
|
|
|
|
switch (s_chanState)
|
|
{
|
|
case CHAN_RX_WAIT_SOF:
|
|
if (ch == BLE_FRAME_SOF)
|
|
{
|
|
s_chanBuf[0] = ch;
|
|
s_chanHave = 1;
|
|
s_chanNeed = BLE_FRAME_HDR_LEN;
|
|
s_chanState = CHAN_RX_HEADER;
|
|
}
|
|
break;
|
|
|
|
case CHAN_RX_HEADER:
|
|
s_chanBuf[s_chanHave++] = ch;
|
|
if (s_chanHave == BLE_FRAME_HDR_LEN)
|
|
{
|
|
uint16_t payLen = (uint16_t)s_chanBuf[3] |
|
|
((uint16_t)s_chanBuf[4] << 8);
|
|
if (payLen > OTA_CHAN_MAX_PAYLOAD)
|
|
{
|
|
s_chanState = CHAN_RX_WAIT_SOF;
|
|
s_chanHave = 0;
|
|
}
|
|
else
|
|
{
|
|
s_chanNeed = (uint16_t)(BLE_FRAME_HDR_LEN + payLen + 2u);
|
|
s_chanState = (s_chanNeed == s_chanHave) ? CHAN_RX_WAIT_SOF
|
|
: CHAN_RX_BODY;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CHAN_RX_BODY:
|
|
s_chanBuf[s_chanHave++] = ch;
|
|
if (s_chanHave == s_chanNeed)
|
|
{
|
|
uint16_t payLen = (uint16_t)s_chanBuf[3] |
|
|
((uint16_t)s_chanBuf[4] << 8);
|
|
uint16_t crc = ota_crc16(&s_chanBuf[1],
|
|
(uint16_t)(4u + payLen));
|
|
uint16_t rxCrc = (uint16_t)s_chanBuf[5 + payLen] |
|
|
((uint16_t)s_chanBuf[5 + payLen + 1] << 8);
|
|
|
|
if (crc == rxCrc)
|
|
{
|
|
app_ota_handle_frame(s_chanBuf[1], s_chanBuf[2],
|
|
&s_chanBuf[5], payLen);
|
|
}
|
|
/* CRC mismatch: silently drop; the host's lockstep
|
|
* resend recovers (its resync relies on our offset
|
|
* report in the next ack) */
|
|
s_chanState = CHAN_RX_WAIT_SOF;
|
|
s_chanHave = 0;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
s_chanState = CHAN_RX_WAIT_SOF;
|
|
s_chanHave = 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* --- BLE OTA characteristic (...e0005): write frames in, read RSP out --- */
|
|
|
|
#define OTA_BLE_RSP_BUF_SIZE 16u /* largest RSP frame = 7B hdr + 6B */
|
|
|
|
static uint8_t s_bleRsp[OTA_BLE_RSP_BUF_SIZE];
|
|
static uint16_t s_bleRspLen;
|
|
|
|
static void ota_ble_sink(const uint8_t* frame, uint16_t len)
|
|
{
|
|
if (len <= OTA_BLE_RSP_BUF_SIZE)
|
|
{
|
|
memcpy(s_bleRsp, frame, len);
|
|
s_bleRspLen = len;
|
|
}
|
|
}
|
|
|
|
void app_ota_chan_rx_ble(const uint8_t* data, uint16_t len)
|
|
{
|
|
app_ota_chan_rx(data, len, ota_ble_sink);
|
|
}
|
|
|
|
const uint8_t* app_ota_chan_rsp_ble(uint16_t* out_len)
|
|
{
|
|
*out_len = s_bleRspLen;
|
|
if (s_bleRspLen != 0u)
|
|
{
|
|
s_rsp_consumed = 1; /* host has fetched the END response */
|
|
}
|
|
s_bleRspLen = 0; /* consumed by the read */
|
|
return s_bleRsp;
|
|
}
|
|
|
|
/* --- UART binary mode (CLI "ota"): RSP frames straight out on TX --- */
|
|
|
|
static void ota_uart_sink(const uint8_t* frame, uint16_t len)
|
|
{
|
|
bsp_usart_tx_lock();
|
|
bsp_usart_write_dma(frame, len);
|
|
bsp_usart_tx_unlock();
|
|
/* Blocking DMA TX: the frame is physically out when we get here */
|
|
s_rsp_consumed = 1;
|
|
}
|
|
|
|
uint8_t app_ota_chan_rx_uart(uint8_t ch)
|
|
{
|
|
uint8_t exit_req;
|
|
|
|
app_ota_chan_rx(&ch, 1, ota_uart_sink);
|
|
exit_req = s_uart_exit_req;
|
|
s_uart_exit_req = 0;
|
|
return exit_req;
|
|
}
|
|
|
|
/**
|
|
* @brief Channel idle timeout helper: dropping the channel state also
|
|
* aborts a half-finished session (called when UART binary mode or
|
|
* the BLE link goes away mid-session).
|
|
*/
|
|
void app_ota_chan_idle(void)
|
|
{
|
|
s_chanState = CHAN_RX_WAIT_SOF;
|
|
s_chanHave = 0;
|
|
}
|