- app_ota.c: after END ok, wait until the RSP is consumed by the host (BLE e0005 read-out / UART TX done) plus 300ms grace before resetting, 2s timeout fallback; polled from the BLE schedule task (rwip_schedule must keep running for the ATT read to be processed) - main.c: app_ota_reset_poll() in the BLE schedule task loop - docs: ble_protocol.md 6.6 note, dev log section 43, AGENTS.md sync
395 lines
12 KiB
C
395 lines
12 KiB
C
/**
|
|
* @file app_ble_proto.c
|
|
* @brief BLE frame protocol frontend implementation (mcm-ddc-ble, over rdtss).
|
|
*
|
|
* RX: byte-stream reassembly state machine (SOF -> header -> payload -> CRC).
|
|
* A frame may span several BLE writes (each write <= att_mtu-3 bytes).
|
|
* Frames with a bad CRC are dropped silently and the receiver resyncs on SOF.
|
|
*
|
|
* TX: CLI output text is chunked into CLI_RSP frames; the raw frame bytes go
|
|
* into the ble_up ring buffer which the BLE schedule task flushes over notify
|
|
* (20B per notify, paced by the notification confirm).
|
|
*
|
|
* Everything below runs in the BLE schedule task context (rx via the rdtss
|
|
* write indication handler) - no locking needed.
|
|
*/
|
|
#include "app_ble_proto.h"
|
|
#include "cli_core.h"
|
|
#include "ble_up.h"
|
|
#include "app_ota.h"
|
|
#include "app_info.h"
|
|
#include "ns_ble.h" /* app_env.max_mtu (negotiated ATT MTU) */
|
|
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
|
|
/* Frame layout (BLE_FRAME_SOF / BLE_FRAME_HDR_LEN live in app_ble_proto.h) */
|
|
#define BLE_FRAME_CRC_LEN 2
|
|
#define BLE_FRAME_OVERHEAD (BLE_FRAME_HDR_LEN + BLE_FRAME_CRC_LEN)
|
|
|
|
/* Limits */
|
|
#define BLE_PROTO_MAX_PAYLOAD 480 /* protocol constraint on the phone side */
|
|
#define BLE_PROTO_RX_BUF_SIZE 512 /* >= one max frame */
|
|
#define BLE_PROTO_TX_FRAME_SIZE 251 /* max BLE packet: 244 payload + overhead */
|
|
#define BLE_PROTO_CLI_LINE_MAX 63 /* CLI_REQ payload limit */
|
|
|
|
/* notify payload is 20B at the default ATT MTU (23-3); one CLI_RSP frame
|
|
* must fit one notify: payload chunk = 20 - frame overhead */
|
|
#define BLE_PROTO_CLI_CHUNK (20 - BLE_FRAME_OVERHEAD)
|
|
|
|
/* RX states */
|
|
enum
|
|
{
|
|
RX_WAIT_SOF = 0,
|
|
RX_HEADER,
|
|
RX_BODY,
|
|
};
|
|
|
|
static uint8_t s_rxBuf[BLE_PROTO_RX_BUF_SIZE];
|
|
static uint16_t s_rxHave;
|
|
static uint16_t s_rxNeed;
|
|
static uint8_t s_rxState;
|
|
|
|
static uint8_t s_txSeq; /* device -> phone seq counter */
|
|
static uint8_t s_txFrame[BLE_PROTO_TX_FRAME_SIZE];
|
|
static uint8_t s_logEn; /* traffic log switch (default off) */
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Traffic log (UART printf; payload preview capped to keep lines short) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
#define BLE_LOG_DUMP_MAX 24u
|
|
|
|
static void proto_dump(const char* dir, uint8_t type, uint8_t seq,
|
|
const uint8_t* payload, uint16_t len)
|
|
{
|
|
uint16_t i;
|
|
uint16_t n = (len > BLE_LOG_DUMP_MAX) ? BLE_LOG_DUMP_MAX : len;
|
|
|
|
printf("[BLE %s] type=0x%02X seq=%u len=%u", dir, type, seq, len);
|
|
for (i = 0; i < n; i++)
|
|
{
|
|
printf(" %02X", payload[i]);
|
|
}
|
|
if (len > n)
|
|
{
|
|
printf(" ...");
|
|
}
|
|
printf("\r\n");
|
|
}
|
|
|
|
void app_ble_proto_log_set(uint8_t on)
|
|
{
|
|
s_logEn = (on != 0) ? 1u : 0u;
|
|
}
|
|
|
|
uint8_t app_ble_proto_log_get(void)
|
|
{
|
|
return s_logEn;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* CRC16-CCITT (init 0xFFFF, poly 0x1021) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static uint16_t proto_crc16(const uint8_t* data, uint16_t len)
|
|
{
|
|
uint16_t crc = 0xFFFF;
|
|
uint16_t i;
|
|
uint8_t bit;
|
|
|
|
for (i = 0; i < len; i++)
|
|
{
|
|
crc ^= (uint16_t)data[i] << 8;
|
|
for (bit = 0; bit < 8; bit++)
|
|
{
|
|
crc = (crc & 0x8000u) ? (uint16_t)((crc << 1) ^ 0x1021u)
|
|
: (uint16_t)(crc << 1);
|
|
}
|
|
}
|
|
return crc;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* TX path */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/**
|
|
* @brief Build one frame and push it into the ble_up uplink ring.
|
|
*/
|
|
int app_ble_proto_send_frame(uint8_t type, uint8_t seq, const uint8_t* payload, uint16_t len)
|
|
{
|
|
uint16_t crc;
|
|
uint16_t frameLen = (uint16_t)(BLE_FRAME_OVERHEAD + len);
|
|
|
|
if (frameLen > BLE_PROTO_TX_FRAME_SIZE)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
s_txFrame[0] = BLE_FRAME_SOF;
|
|
s_txFrame[1] = type;
|
|
s_txFrame[2] = seq;
|
|
s_txFrame[3] = (uint8_t)(len & 0xFFu);
|
|
s_txFrame[4] = (uint8_t)(len >> 8);
|
|
if (len != 0)
|
|
{
|
|
memcpy(&s_txFrame[5], payload, len);
|
|
}
|
|
crc = proto_crc16(&s_txFrame[1], (uint16_t)(4u + len)); /* TYPE..PAYLOAD (incl. SEQ+LEN) */
|
|
s_txFrame[5 + len] = (uint8_t)(crc & 0xFFu);
|
|
s_txFrame[5 + len + 1] = (uint8_t)(crc >> 8);
|
|
|
|
if (s_logEn)
|
|
{
|
|
proto_dump("TX", type, seq, payload, len);
|
|
}
|
|
ble_up_write(s_txFrame, frameLen);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief cli_out_fn for the BLE frontend: chunk response text into
|
|
* CLI_RSP frames (one frame per 20B notify at the default MTU).
|
|
*/
|
|
static void proto_cli_out(const char* str)
|
|
{
|
|
while (*str != '\0')
|
|
{
|
|
uint16_t n = (uint16_t)strlen(str);
|
|
if (n > BLE_PROTO_CLI_CHUNK)
|
|
{
|
|
n = BLE_PROTO_CLI_CHUNK;
|
|
}
|
|
(void)app_ble_proto_send_frame(BLE_FRAME_CLI_RSP, s_txSeq++, (const uint8_t*)str, n);
|
|
str += n;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* CLI over the read/write characteristic: the phone writes a raw */
|
|
/* command line (write-with-response) and reads the output text back */
|
|
/* in sequential chunks. Each ATT read of the characteristic returns */
|
|
/* the next slice (<= att_mtu-2, so the client never issues a Read */
|
|
/* Blob - the blob offset is not forwarded to the app and the ROM */
|
|
/* GATT would slice the wrong window). A 0-length read marks the end */
|
|
/* of the response; writing a new command rewinds the read offset. */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
#define CLI_RW_CMD_MAX 63 /* same line limit as the frame CLI */
|
|
#define CLI_RW_RSP_MAX 1024 /* total captured output per command */
|
|
|
|
static uint8_t s_cliRsp[CLI_RW_RSP_MAX];
|
|
static uint16_t s_cliRspLen;
|
|
static uint16_t s_cliRspOff; /* sequential read offset */
|
|
|
|
/* cli_out_fn capturing the command output into s_cliRsp */
|
|
static void cli_rsp_out(const char* str)
|
|
{
|
|
while (*str != '\0' && s_cliRspLen < CLI_RW_RSP_MAX)
|
|
{
|
|
s_cliRsp[s_cliRspLen++] = (uint8_t)*str++;
|
|
}
|
|
}
|
|
|
|
void app_ble_proto_cli_exec(const uint8_t* cmd, uint16_t len)
|
|
{
|
|
static char s_line[CLI_RW_CMD_MAX + 1];
|
|
int status = 1; /* default: rejected */
|
|
|
|
s_cliRspLen = 0;
|
|
s_cliRspOff = 0;
|
|
if (len > 0 && len <= CLI_RW_CMD_MAX)
|
|
{
|
|
memcpy(s_line, cmd, len);
|
|
s_line[len] = '\0';
|
|
/* same set/exec/restore policy as the frame CLI path */
|
|
cli_set_output(cli_rsp_out);
|
|
status = cli_exec_line(s_line);
|
|
cli_set_output(NULL);
|
|
}
|
|
if (s_cliRspLen == 0)
|
|
{
|
|
const char* fb = (status == 0) ? "ok\r\n" : "unknown command\r\n";
|
|
uint16_t n = (uint16_t)strlen(fb);
|
|
memcpy(s_cliRsp, fb, n);
|
|
s_cliRspLen = n;
|
|
}
|
|
|
|
if (s_logEn)
|
|
{
|
|
uint16_t i;
|
|
printf("[BLE] cli exec: \"");
|
|
for (i = 0; i < len && i < CLI_RW_CMD_MAX; i++)
|
|
{
|
|
printf("%c", (cmd[i] >= 32 && cmd[i] < 127) ? cmd[i] : '.');
|
|
}
|
|
printf("\" -> %uB rsp\r\n", s_cliRspLen);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Serve the next chunk of the last command's output. Called once per
|
|
* ATT read of the CLI characteristic; a 0-length return marks EOF.
|
|
*/
|
|
const uint8_t* app_ble_proto_cli_rsp(uint16_t* out_len)
|
|
{
|
|
/* Keep the slice below att_mtu-1 so the client sees a short read and
|
|
* never starts a Read Blob sequence (see the block comment above) */
|
|
uint16_t mtu = app_env.max_mtu;
|
|
uint16_t chunk = (mtu >= 24u) ? (uint16_t)(mtu - 2u) : 20u;
|
|
uint16_t remain = (uint16_t)(s_cliRspLen - s_cliRspOff);
|
|
const uint8_t* p = s_cliRsp + s_cliRspOff;
|
|
|
|
if (remain > chunk)
|
|
{
|
|
remain = chunk;
|
|
}
|
|
s_cliRspOff = (uint16_t)(s_cliRspOff + remain);
|
|
*out_len = remain;
|
|
return p;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* RX path */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/**
|
|
* @brief Handle one complete, CRC-valid frame.
|
|
*/
|
|
static void proto_dispatch_frame(uint8_t type, uint8_t seq, const uint8_t* payload, uint16_t len)
|
|
{
|
|
if (s_logEn)
|
|
{
|
|
proto_dump("RX", type, seq, payload, len);
|
|
}
|
|
|
|
switch (type)
|
|
{
|
|
case BLE_FRAME_CLI_REQ:
|
|
{
|
|
static char s_line[BLE_PROTO_CLI_LINE_MAX + 1];
|
|
int status = 1; /* default: rejected */
|
|
|
|
if (len <= BLE_PROTO_CLI_LINE_MAX)
|
|
{
|
|
memcpy(s_line, payload, len);
|
|
s_line[len] = '\0';
|
|
/* Route the CLI core output to BLE frames for this command.
|
|
* Simple mutual exclusion with the UART frontend: the UART
|
|
* frontend forces output back to default before each of its
|
|
* own cli_exec_line() calls (see cli_core.c note). */
|
|
cli_set_output(proto_cli_out);
|
|
status = cli_exec_line(s_line);
|
|
cli_set_output(NULL);
|
|
}
|
|
{
|
|
uint8_t st = (uint8_t)status;
|
|
(void)app_ble_proto_send_frame(BLE_FRAME_CLI_RSP_END, seq, &st, 1);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case BLE_FRAME_OTA_BEGIN:
|
|
case BLE_FRAME_OTA_DATA:
|
|
case BLE_FRAME_OTA_END:
|
|
app_ota_handle_frame(type, seq, payload, len);
|
|
break;
|
|
|
|
case BLE_FRAME_INFO_QUERY:
|
|
app_info_handle_query(seq, payload, len);
|
|
break;
|
|
|
|
default:
|
|
/* unknown types: drop */
|
|
break;
|
|
}
|
|
}
|
|
|
|
void app_ble_proto_rx_bytes(const uint8_t* data, uint16_t len)
|
|
{
|
|
uint16_t i;
|
|
|
|
for (i = 0; i < len; i++)
|
|
{
|
|
uint8_t ch = data[i];
|
|
|
|
switch (s_rxState)
|
|
{
|
|
case RX_WAIT_SOF:
|
|
if (ch == BLE_FRAME_SOF)
|
|
{
|
|
s_rxBuf[0] = ch;
|
|
s_rxHave = 1;
|
|
s_rxNeed = BLE_FRAME_HDR_LEN;
|
|
s_rxState = RX_HEADER;
|
|
}
|
|
break;
|
|
|
|
case RX_HEADER:
|
|
s_rxBuf[s_rxHave++] = ch;
|
|
if (s_rxHave == BLE_FRAME_HDR_LEN)
|
|
{
|
|
uint16_t payLen = (uint16_t)s_rxBuf[3] | ((uint16_t)s_rxBuf[4] << 8);
|
|
if (payLen > BLE_PROTO_MAX_PAYLOAD)
|
|
{
|
|
/* implausible length: drop and resync */
|
|
s_rxState = RX_WAIT_SOF;
|
|
s_rxHave = 0;
|
|
}
|
|
else
|
|
{
|
|
s_rxNeed = (uint16_t)(BLE_FRAME_OVERHEAD + payLen);
|
|
s_rxState = RX_BODY;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case RX_BODY:
|
|
s_rxBuf[s_rxHave++] = ch;
|
|
if (s_rxHave == s_rxNeed)
|
|
{
|
|
uint16_t payLen = (uint16_t)s_rxBuf[3] | ((uint16_t)s_rxBuf[4] << 8);
|
|
uint16_t crcCalc = proto_crc16(&s_rxBuf[1], (uint16_t)(4u + payLen));
|
|
uint16_t crcRecv = (uint16_t)s_rxBuf[5 + payLen] |
|
|
((uint16_t)s_rxBuf[5 + payLen + 1] << 8);
|
|
|
|
if (crcCalc == crcRecv)
|
|
{
|
|
proto_dispatch_frame(s_rxBuf[1], s_rxBuf[2], &s_rxBuf[5], payLen);
|
|
}
|
|
/* bad CRC: drop silently, resync on next SOF */
|
|
s_rxState = RX_WAIT_SOF;
|
|
s_rxHave = 0;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
s_rxState = RX_WAIT_SOF;
|
|
s_rxHave = 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* State management */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
void app_ble_proto_init(void)
|
|
{
|
|
s_rxState = RX_WAIT_SOF;
|
|
s_rxHave = 0;
|
|
s_rxNeed = 0;
|
|
s_txSeq = 0;
|
|
}
|
|
|
|
void app_ble_proto_on_disconnect(void)
|
|
{
|
|
s_rxState = RX_WAIT_SOF; /* drop a half-received frame */
|
|
s_rxHave = 0;
|
|
s_cliRspOff = 0; /* rewind the chunked CLI read */
|
|
ble_up_reset();
|
|
app_ota_abort(); /* abort any on-going OTA session */
|
|
}
|