fix: END ACK regression — readPoll pintar + INIT_ACK_INDEX 0xFFFE + binary_parser.c git track
Akar masalah: -4a6ef8cmenambah binary_parser_reset() di handler re-deploy SERIAL_BRIDGE yang menghapus expected_total_crc setelah INIT — menyebabkan CRC mismatch di END. Source1d3a489sudah menghapus reset-nya, tapi device butuh rebuild. -cad90ebmerutekan DATA melalui sendCommand() dengan readPoll state===1 yang selalu true saat DATA — write gagal dilaporkan sukses. Perbaikan: 1. readPoll pintar untuk DATA: state===1 tidak lagi dianggap sukses. readPoll hanya fast-fail di state>=5; deadline fallback hanya ok jika writeValueWithResponse sukses (GATT write response = bukti delivery). 2. INIT_ACK_INDEX=0xFFFE (Fase 3): inisialisasi INIT dengan index unik menghilangkan tabrakan dengan DATA[0] (index 0). binary_parser.c echo index recv di ACK INIT. Guard handleFlashingNotification diperluas. 3. pendingAcks.clear() di awal deployHex mencegah poisoning antar deploy. 4. Track firmware inti ke git: binary_parser.c, checksum.c, stk500v1.c, usb_host.c + headers + CMakeLists + sdkconfig + partitions. Build: ESP-IDF v6, ESP32-S3, flash via /dev/ttyACM0
This commit is contained in:
parent
d146e445d6
commit
c20b2d5a8e
|
|
@ -3,7 +3,7 @@ import {
|
|||
BLE_CHAR_FLASHING_UUID,
|
||||
BLE_CHAR_SERIAL_UUID,
|
||||
CMD_INIT, CMD_DATA, CMD_END, CMD_ACK, CMD_ERR,
|
||||
CHUNK_SIZE, BLE_TIMEOUT_MS, END_TIMEOUT_MS, END_ACK_INDEX, MAX_RETRIES,
|
||||
CHUNK_SIZE, BLE_TIMEOUT_MS, END_TIMEOUT_MS, END_ACK_INDEX, INIT_ACK_INDEX, MAX_RETRIES,
|
||||
type DeployProgress, type BLEACKResponse
|
||||
} from '$types/deployer';
|
||||
|
||||
|
|
@ -155,11 +155,13 @@ export class BLEHardwareDeployer {
|
|||
this.ackResolvers.delete(index);
|
||||
clearTimeout(pending.timer);
|
||||
pending.resolve(ack);
|
||||
} else if (this.ackResolvers.has(0xFFFF) && index !== 0xFFFF) {
|
||||
} else if ((this.ackResolvers.has(0xFFFF) || this.ackResolvers.has(INIT_ACK_INDEX)) && index !== 0xFFFF && index !== INIT_ACK_INDEX) {
|
||||
// No one waiting for this specific index right now,
|
||||
// but someone IS waiting for END (0xFFFF) — don't
|
||||
// let a stale chunk ACK resolve it.
|
||||
logAck(cmdName, `index=${index} no resolver, END waitForAck(0xFFFF) active — dropping stale ACK`);
|
||||
// but someone IS waiting for END (0xFFFF) or INIT
|
||||
// (INIT_ACK_INDEX) — don't let a stale chunk ACK
|
||||
// resolve them. Only END/INIT sentinel ACKs are
|
||||
// relevant while these waits are active.
|
||||
logAck(cmdName, `index=${index} no resolver, waitForAck(0xFFFF/0xFFFE) active — dropping stale ACK`);
|
||||
} else {
|
||||
// Cache for a future waitForAck call
|
||||
logAck(cmdName, `index=${index} no resolver — caching as pendingAck`);
|
||||
|
|
@ -193,9 +195,13 @@ export class BLEHardwareDeployer {
|
|||
|
||||
console.log(`[BLE] deployHex: ${binaryData.length} bytes, ${totalChunks} chunks, CRC=0x${totalCRC.toString(16)}`);
|
||||
|
||||
/* Clear stale ACKs from a previous deploy (prevents cross-deploy
|
||||
* poisoning where a late ACK from deploy N resolves deploy N+1). */
|
||||
this.pendingAcks.clear();
|
||||
|
||||
onProgress({ state: 'transferring', message: 'Mengirim INIT...', totalChunks, completedChunks: 0 });
|
||||
console.log('[BLE] D: sending INIT...');
|
||||
await this.sendCommand(CMD_INIT, 0, this.uint32ToBytes(totalCRC));
|
||||
await this.sendCommand(CMD_INIT, INIT_ACK_INDEX, this.uint32ToBytes(totalCRC));
|
||||
console.log('[BLE] D: INIT done');
|
||||
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
|
|
@ -246,6 +252,7 @@ export class BLEHardwareDeployer {
|
|||
], 4 + data.length);
|
||||
|
||||
const ackTimeout = timeoutMs ?? BLE_TIMEOUT_MS;
|
||||
let writeSucceeded = false;
|
||||
console.log(`[BLE-CMD] ${cmdName} idx=${index}: waitForAck timeout=${ackTimeout}ms start`);
|
||||
const ackPromise = this.waitForAck(index, ackTimeout);
|
||||
try {
|
||||
|
|
@ -255,6 +262,7 @@ export class BLEHardwareDeployer {
|
|||
3000,
|
||||
'writeValueWithResponse'
|
||||
);
|
||||
writeSucceeded = true;
|
||||
console.log(`[BLE-CMD] ${cmdName} idx=${index}: write done, now waiting for ACK...`);
|
||||
} catch (e) {
|
||||
console.log(`[BLE-CMD] ${cmdName} idx=${index}: write failed, still waiting for ACK`, e);
|
||||
|
|
@ -347,10 +355,12 @@ export class BLEHardwareDeployer {
|
|||
return;
|
||||
}
|
||||
|
||||
/* INIT/DATA: Same Android notify idle-drop bug affects these too.
|
||||
* Race ackPromise against readValue() state poll as fallback.
|
||||
* INIT success: firmware state >= RECEIVING(1) (not IDLE/ERROR).
|
||||
* DATA success: firmware state == RECEIVING(1). */
|
||||
/* INIT: Firmware sets state to RECEIVING(1) on success — poll for it.
|
||||
* DATA: ACK notify confirms per-chunk CRC. Since state=1 is always
|
||||
* true during DATA phase, readPoll cannot confirm a specific chunk.
|
||||
* Instead, readPoll fast-fails on ERROR state (>=5). At deadline, if
|
||||
* write succeeded (writeValueWithResponse confirms firmware processed
|
||||
* the chunk), return ok. Otherwise return timeout for retry. */
|
||||
if (cmd === CMD_INIT || cmd === CMD_DATA) {
|
||||
const readPoll = (async () => {
|
||||
const deadline = Date.now() + ackTimeout;
|
||||
|
|
@ -364,25 +374,16 @@ export class BLEHardwareDeployer {
|
|||
const state = dv.getUint8(0);
|
||||
console.log(`[BLE-CMD] ${cmdName}: readValue state=${state}`);
|
||||
|
||||
/* INIT: any non-IDLE(0), non-ERROR(5/6) state means command was processed.
|
||||
* DATA: state must stay RECEIVING(1) — error if >=5. */
|
||||
if (cmd === CMD_INIT) {
|
||||
/* Only RECEIVING(1) means INIT was processed.
|
||||
* Don't accept FLASHING(3) or SERIAL_BRIDGE(4) —
|
||||
* those are stale states from a previous deploy. */
|
||||
if (state === 1) {
|
||||
console.log(`[BLE-CMD] ${cmdName}: state=1 (RECEIVING) — INIT confirmed via read poll`);
|
||||
return 'ok' as const;
|
||||
}
|
||||
} else { /* DATA */
|
||||
if (state === 1) {
|
||||
console.log(`[BLE-CMD] ${cmdName}: state=1 (RECEIVING) — DATA confirmed via read poll`);
|
||||
return 'ok' as const;
|
||||
}
|
||||
}
|
||||
if (state >= 5) {
|
||||
throw new Error(`Firmware error (state=${state})`);
|
||||
}
|
||||
/* INIT: Only RECEIVING(1) means command was processed.
|
||||
* Don't accept FLASHING(3) or SERIAL_BRIDGE(4) —
|
||||
* those are stale states from a previous deploy. */
|
||||
if (cmd === CMD_INIT && state === 1) {
|
||||
console.log(`[BLE-CMD] ${cmdName}: state=1 (RECEIVING) — INIT confirmed via read poll`);
|
||||
return 'ok' as const;
|
||||
}
|
||||
} catch (e) {
|
||||
if (!this.isConnected) {
|
||||
throw new Error('Koneksi BLE terputus');
|
||||
|
|
@ -391,6 +392,12 @@ export class BLEHardwareDeployer {
|
|||
}
|
||||
await new Promise<void>(r => setTimeout(r, 200));
|
||||
}
|
||||
/* Deadline reached. For DATA: writeValueWithResponse confirms
|
||||
* the firmware processed the chunk (GATT write response). */
|
||||
if (cmd === CMD_DATA && writeSucceeded) {
|
||||
console.log(`[BLE-CMD] ${cmdName}: deadline, write succeeded — treating as ok`);
|
||||
return 'ok' as const;
|
||||
}
|
||||
console.log(`[BLE-CMD] ${cmdName}: readPoll deadline passed (${ackTimeout}ms)`);
|
||||
return 'timeout' as const;
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
export type DeployState =
|
||||
| 'idle'
|
||||
| 'compiling'
|
||||
| 'pairing'
|
||||
| 'transferring'
|
||||
| 'flashing'
|
||||
| 'serial_bridge'
|
||||
| 'success'
|
||||
| 'error';
|
||||
|
||||
export interface DeployProgress {
|
||||
state: DeployState;
|
||||
message: string;
|
||||
totalChunks: number;
|
||||
completedChunks: number;
|
||||
bytesPerSecond?: number;
|
||||
}
|
||||
|
||||
export interface BLEACKResponse {
|
||||
command: number;
|
||||
index: number;
|
||||
status: 'OK' | 'ERROR';
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const BLE_SERVICE_UUID = '56454c58-494f-0000-0000-000000000001';
|
||||
export const BLE_CHAR_FLASHING_UUID = '56454c58-494f-0000-0000-000000000002';
|
||||
export const BLE_CHAR_SERIAL_UUID = '56454c58-494f-0000-0000-000000000003';
|
||||
|
||||
export const CMD_INIT = 0x01;
|
||||
export const CMD_DATA = 0x02;
|
||||
export const CMD_END = 0x03;
|
||||
export const CMD_ACK = 0x04;
|
||||
export const CMD_ERR = 0x05;
|
||||
|
||||
export const CHUNK_SIZE = 240;
|
||||
export const BLE_TIMEOUT_MS = 5000;
|
||||
export const END_TIMEOUT_MS = 30000;
|
||||
export const MAX_RETRIES = 3;
|
||||
/** Unique ACK index for INIT (disambiguates from DATA chunk index 0). */
|
||||
export const INIT_ACK_INDEX = 0xFFFE;
|
||||
/** Unique ACK index sent by firmware after flash completes (disambiguates from INIT ACK). */
|
||||
export const END_ACK_INDEX = 0xFFFF;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(velxio-deployer)
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
# Velxio BLE Deployer — Firmware ESP32-S3
|
||||
|
||||
Firmware untuk ESP32-S3 N16R8 yang bertindak sebagai **BLE-to-USB bridge** untuk flashing Arduino Uno/Nano via Web Bluetooth.
|
||||
|
||||
## Persyaratan
|
||||
|
||||
- [ESP-IDF v6.0](https://docs.espressif.com/projects/esp-idf/en/v6.0/esp32s3/get-started/index.html)
|
||||
- ESP32-S3 dev board (N16R8 — 16MB Flash + 8MB PSRAM)
|
||||
- Kabel USB-C (data) untuk menghubungkan ke Arduino (USB OTG GPIO19/20)
|
||||
- Debug log via UART0 (USB-to-UART bridge devkit)
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Export ESP-IDF environment
|
||||
source /home/a2nr/Downloads/lms-c/esp-idf-v6/export.sh
|
||||
|
||||
# Build
|
||||
idf.py build
|
||||
|
||||
# Flash (ganti /dev/ttyACM0 sesuai port)
|
||||
idf.py -p /dev/ttyACM0 flash monitor
|
||||
```
|
||||
|
||||
## Konfigurasi
|
||||
|
||||
Variabel utama di `sdkconfig.defaults`:
|
||||
|
||||
| Konfigurasi | Nilai | Keterangan |
|
||||
|------------|-------|------------|
|
||||
| `CONFIG_IDF_TARGET` | `esp32s3` | Target chip (WAJIB, bukan esp32) |
|
||||
| `CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME` | `Velxio-Deployer` | Nama BLE yang tampil di browser |
|
||||
| `CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU` | `255` | Ukuran MTU BLE |
|
||||
| `CONFIG_SPIRAM` | `y` | PSRAM enabled |
|
||||
| `CONFIG_SPIRAM_MODE_OCT` | `y` | Octal mode (N16R8) |
|
||||
| `CONFIG_ESPTOOLPY_FLASHSIZE_16MB` | `y` | Flash 16MB |
|
||||
| `CONFIG_CHERRYUSB_HOST_CDC_ACM` | `y` | USB Host CDC (CherryUSB) |
|
||||
| `CONFIG_ESP_CONSOLE_UART_DEFAULT` | `y` | Debug via UART0 (bukan USB-JTAG) |
|
||||
|
||||
## Partisi Flash
|
||||
|
||||
| Partisi | Ukuran | Fungsi |
|
||||
|---------|--------|--------|
|
||||
| ota_0 | 3 MB | Firmware utama |
|
||||
| ota_1 | 3 MB | OTA update |
|
||||
| storage | ~10 MB | SPIFFS untuk log/filesystem |
|
||||
|
||||
## Arsitektur Komponen
|
||||
|
||||
```
|
||||
main.c
|
||||
├── ble_service.c # NimBLE peripheral (2 karakteristik)
|
||||
├── binary_parser.c # Parser payload binary (INIT/DATA/END)
|
||||
├── checksum.c # CRC32
|
||||
├── usb_host.c # CherryUSB Host (CDC) — belum lengkap
|
||||
├── stk500v1.c # STK500v1 flashing protocol — belum lengkap
|
||||
├── state_machine.c # Finite state machine
|
||||
├── serial_bridge.c # USB CDC ↔ BLE Notify bridge
|
||||
└── led_button.c # LED RGB + Retry button
|
||||
```
|
||||
|
||||
## Protokol BLE
|
||||
|
||||
```
|
||||
Service: 56454c58-494f-0000-0000-000000000001
|
||||
Flashing: 56454c58-494f-0000-0000-000000000002 (Write+Resp + Notify)
|
||||
Serial: 56454c58-494f-0000-0000-000000000003 (WriteWO+Resp + Notify)
|
||||
```
|
||||
|
||||
Payload format: `[CMD:1][Index:2 LE][Len:1][Data:N ≤240][CRC32:4 LE]`
|
||||
|
||||
### Catatan UUID (PENTING)
|
||||
|
||||
UUID di firmware menggunakan `BLE_UUID128_INIT` dengan byte order **little-endian**:
|
||||
```c
|
||||
// 56454C58-494F-0000-0000-000000000001
|
||||
BLE_UUID128_INIT(
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x4F, 0x49, 0x58, 0x4C, 0x45, 0x56
|
||||
)
|
||||
```
|
||||
|
||||
## Testing (tanpa Web Bluetooth)
|
||||
|
||||
Gunakan `nRF Connect` Android untuk test BLE:
|
||||
1. Scan → pilih "Velxio-Deployer" (nama pendek "Velxio" di adv data)
|
||||
2. Connect → MTU otomatis ter-negosiasi ke 255
|
||||
3. Subscribe ke karakteristik Flashing (Notify)
|
||||
4. Kirim payload INIT: `01 00 00 04 [CRC-total(4)] [CRC-packet(4)]`
|
||||
5. Kirim payload DATA: `02 [idx(2)] [len] [data...] [CRC(4)]`
|
||||
6. Kirim payload END: `03 00 00 04 [CRC-total(4)] [CRC-packet(4)]`
|
||||
|
||||
## Status Development
|
||||
|
||||
| Fase | Status | Keterangan |
|
||||
|------|--------|------------|
|
||||
| F0 Environment | Selesai | ESP-IDF v6.0 terinstall |
|
||||
| F1 Project config | Selesai | Target esp32s3, PSRAM, CherryUSB |
|
||||
| F2 BLE service | Selesai | UUID branded hex, adv data, MTU 255 |
|
||||
| F3 Protocol/ACK | Selesai | END ACK setelah flash, parse error fix |
|
||||
| F4 STK500v1 | Belum | Implementasi lengkap |
|
||||
| F5 USB Host | Belum | CherryUSB RX claim/unclaim |
|
||||
| F6 Serial bridge | Belum | Throttle + LED polish |
|
||||
| F7 Frontend | Selesai | UUID, chunk 240, requestMTU |
|
||||
| F8 Dokumen | Selesai | Dokumen ini |
|
||||
| F9 Testing | Belum | Progressive integration test |
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
dependencies:
|
||||
cherry-embedded/cherryusb:
|
||||
component_hash: b8b0db4ed23d32e01ec41138600fb014bcf060a34b541daba1fca6ed22a61dfc
|
||||
dependencies: []
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 1.6.1
|
||||
idf:
|
||||
source:
|
||||
type: idf
|
||||
version: 6.0.1
|
||||
direct_dependencies:
|
||||
- cherry-embedded/cherryusb
|
||||
- idf
|
||||
manifest_hash: 2c2aa30e38426bfc9f94d294becfce085d82f528b9a906d311652fd3ff99b537
|
||||
target: esp32s3
|
||||
version: 3.0.0
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
idf_component_register(
|
||||
SRCS
|
||||
main.c
|
||||
ble_service.c
|
||||
binary_parser.c
|
||||
checksum.c
|
||||
usb_host.c
|
||||
stk500v1.c
|
||||
state_machine.c
|
||||
serial_bridge.c
|
||||
led_button.c
|
||||
INCLUDE_DIRS
|
||||
.
|
||||
)
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "binary_parser.h"
|
||||
#include "checksum.h"
|
||||
#include "ble_service.h"
|
||||
|
||||
static const char *TAG = "BIN_PARSER";
|
||||
|
||||
static parser_state_t state = PARSER_IDLE;
|
||||
static uint8_t *hex_buffer = NULL;
|
||||
static size_t buffer_offset = 0;
|
||||
static size_t buffer_size = 0;
|
||||
static uint32_t expected_total_crc = 0;
|
||||
static int total_chunks = 0;
|
||||
static int received_chunks = 0;
|
||||
|
||||
void binary_parser_init(void)
|
||||
{
|
||||
hex_buffer = (uint8_t *)heap_caps_malloc(MAX_HEX_SIZE, MALLOC_CAP_SPIRAM);
|
||||
if (hex_buffer) {
|
||||
ESP_LOGI(TAG, "PSRAM buffer allocated: %d bytes at %p", MAX_HEX_SIZE, hex_buffer);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to allocate PSRAM buffer!");
|
||||
hex_buffer = malloc(MAX_HEX_SIZE);
|
||||
if (hex_buffer) {
|
||||
ESP_LOGW(TAG, "Fallback to SRAM buffer: %d bytes", MAX_HEX_SIZE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parser_state_t binary_parser_process_packet(uint8_t *payload, size_t len)
|
||||
{
|
||||
if (len < 8) return PARSER_ERROR;
|
||||
if (!hex_buffer) return PARSER_ERROR;
|
||||
|
||||
uint8_t cmd = payload[0];
|
||||
uint16_t index = payload[1] | (payload[2] << 8);
|
||||
uint8_t data_len = payload[3];
|
||||
|
||||
if (4 + data_len + 4 > len) return PARSER_ERROR;
|
||||
|
||||
uint8_t *data = payload + 4;
|
||||
uint32_t received_crc = (uint32_t)payload[4 + data_len] |
|
||||
((uint32_t)payload[4 + data_len + 1] << 8) |
|
||||
((uint32_t)payload[4 + data_len + 2] << 16) |
|
||||
((uint32_t)payload[4 + data_len + 3] << 24);
|
||||
|
||||
switch (cmd) {
|
||||
case CMD_INIT: {
|
||||
if (data_len >= 4) {
|
||||
expected_total_crc = (uint32_t)data[0] |
|
||||
((uint32_t)data[1] << 8) |
|
||||
((uint32_t)data[2] << 16) |
|
||||
((uint32_t)data[3] << 24);
|
||||
}
|
||||
buffer_offset = 0;
|
||||
buffer_size = 0;
|
||||
received_chunks = 0;
|
||||
total_chunks = 0;
|
||||
memset(hex_buffer, 0, MAX_HEX_SIZE);
|
||||
state = PARSER_RECEIVING;
|
||||
ESP_LOGI(TAG, "INIT: expected total CRC = 0x%08lX", (unsigned long)expected_total_crc);
|
||||
|
||||
/* Echo the received index so the webapp can distinguish INIT ACK
|
||||
* from DATA chunk-0 ACK (both would be index 0 if hardcoded).
|
||||
* Webapp sends INIT with INIT_ACK_INDEX (0xFFFE) for uniqueness. */
|
||||
uint8_t ack[] = {CMD_ACK, index & 0xFF, (index >> 8) & 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
ble_service_send_notify_flashing(ack, sizeof(ack));
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_DATA: {
|
||||
if (state != PARSER_RECEIVING) return PARSER_ERROR;
|
||||
|
||||
uint32_t chunk_crc = checksum_crc32(data, data_len);
|
||||
if (chunk_crc != received_crc) {
|
||||
ESP_LOGE(TAG, "CRC mismatch chunk %d: expected 0x%08lX, got 0x%08lX",
|
||||
index, (unsigned long)received_crc, (unsigned long)chunk_crc);
|
||||
uint8_t err[] = {CMD_ERR, index & 0xFF, (index >> 8) & 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
ble_service_send_notify_flashing(err, sizeof(err));
|
||||
return PARSER_ERROR;
|
||||
}
|
||||
|
||||
if (buffer_offset + data_len <= MAX_HEX_SIZE) {
|
||||
memcpy(hex_buffer + buffer_offset, data, data_len);
|
||||
buffer_offset += data_len;
|
||||
received_chunks++;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Buffer overflow!");
|
||||
state = PARSER_ERROR;
|
||||
return PARSER_ERROR;
|
||||
}
|
||||
|
||||
uint8_t ack[] = {CMD_ACK, index & 0xFF, (index >> 8) & 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
ble_service_send_notify_flashing(ack, sizeof(ack));
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_END: {
|
||||
buffer_size = buffer_offset;
|
||||
state = PARSER_COMPLETE;
|
||||
ESP_LOGI(TAG, "END: %d bytes in %d chunks, buffer_size=%d",
|
||||
buffer_size, received_chunks, (int)buffer_size);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return PARSER_ERROR;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
uint32_t binary_parser_get_total_crc(void)
|
||||
{
|
||||
return expected_total_crc;
|
||||
}
|
||||
|
||||
uint8_t *binary_parser_get_buffer(void)
|
||||
{
|
||||
return hex_buffer;
|
||||
}
|
||||
|
||||
size_t binary_parser_get_buffer_size(void)
|
||||
{
|
||||
return buffer_size;
|
||||
}
|
||||
|
||||
void binary_parser_reset(void)
|
||||
{
|
||||
state = PARSER_IDLE;
|
||||
buffer_offset = 0;
|
||||
buffer_size = 0;
|
||||
received_chunks = 0;
|
||||
total_chunks = 0;
|
||||
expected_total_crc = 0;
|
||||
}
|
||||
|
||||
parser_state_t binary_parser_get_state(void)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* @file binary_parser.h
|
||||
* @brief Binary payload parser for the BLE flashing protocol.
|
||||
*
|
||||
* Implements the 4-command protocol:
|
||||
* INIT (0x01) — start transfer, receive expected total CRC
|
||||
* DATA (0x02) — chunk of hex binary with per-chunk CRC32
|
||||
* END (0x03) — finalise, verify accumulated CRC against expected
|
||||
* ACK (0x04) — acknowledgement (sent by firmware)
|
||||
* ERR (0x05) — error indication (sent by firmware)
|
||||
*
|
||||
* Payload format: [CMD:1][Index:2][Len:1][Data:N][CRC32:4]
|
||||
*
|
||||
* Buffer allocated in PSRAM via heap_caps_malloc(MALLOC_CAP_SPIRAM).
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define CMD_INIT 0x01
|
||||
#define CMD_DATA 0x02
|
||||
#define CMD_END 0x03
|
||||
#define CMD_ACK 0x04
|
||||
#define CMD_ERR 0x05
|
||||
|
||||
/** Maximum hex binary buffer size (256 KB — fits largest Arduino sketch). */
|
||||
#define MAX_HEX_SIZE (256 * 1024)
|
||||
|
||||
/** Parser finite state. */
|
||||
typedef enum {
|
||||
PARSER_IDLE, /**< Waiting for INIT. */
|
||||
PARSER_RECEIVING, /**< Actively receiving DATA chunks. */
|
||||
PARSER_COMPLETE, /**< All data received and CRC verified. */
|
||||
PARSER_ERROR /**< CRC mismatch or protocol violation. */
|
||||
} parser_state_t;
|
||||
|
||||
/**
|
||||
* @brief Single binary packet descriptor (for inspection/debugging).
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t command; /**< CMD_INIT / CMD_DATA / CMD_END */
|
||||
uint16_t index; /**< Chunk index (little-endian) */
|
||||
uint8_t length; /**< Number of data bytes in this packet */
|
||||
uint8_t *data; /**< Pointer to data portion */
|
||||
uint32_t crc32; /**< CRC32 value from packet trailer */
|
||||
} binary_packet_t;
|
||||
|
||||
void binary_parser_init(void);
|
||||
parser_state_t binary_parser_process_packet(uint8_t *payload, size_t len);
|
||||
uint32_t binary_parser_get_total_crc(void);
|
||||
uint8_t *binary_parser_get_buffer(void);
|
||||
size_t binary_parser_get_buffer_size(void);
|
||||
void binary_parser_reset(void);
|
||||
parser_state_t binary_parser_get_state(void);
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* @file ble_service.h
|
||||
* @brief BLE Peripheral service using NimBLE stack.
|
||||
*
|
||||
* Manages advertising, connection, and two custom GATT characteristics:
|
||||
* - Flashing (Write with Response + Notify): for binary payload transfer
|
||||
* - Serial (Write Without Response + Notify): for transparent UART bridge
|
||||
*
|
||||
* Service UUID: 56454c58-494f-0000-0000-000000000001
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define BLE_SERVICE_UUID "56454c58-494f-0000-0000-000000000001"
|
||||
#define BLE_CHAR_FLASHING_UUID "56454c58-494f-0000-0000-000000000002"
|
||||
#define BLE_CHAR_SERIAL_UUID "56454c58-494f-0000-0000-000000000003"
|
||||
|
||||
/**
|
||||
* @brief Callback for incoming data on the Flashing characteristic.
|
||||
* @param data Pointer to received payload bytes
|
||||
* @param len Number of bytes received
|
||||
*/
|
||||
typedef void (*ble_data_cb_t)(uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Callback for incoming data on the Serial characteristic (Write Without Response).
|
||||
*/
|
||||
typedef void (*ble_serial_cb_t)(uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Initialize NimBLE stack, register GATT services, start advertising.
|
||||
*/
|
||||
void ble_service_init(void);
|
||||
|
||||
/**
|
||||
* @brief Register callback for Flashing characteristic write events.
|
||||
*/
|
||||
void ble_service_set_flashing_callback(ble_data_cb_t cb);
|
||||
|
||||
/**
|
||||
* @brief Register callback for Serial characteristic write events.
|
||||
* Receives raw bytes from Webapp → forwarded to Arduino via USB CDC.
|
||||
*/
|
||||
void ble_service_set_serial_callback(ble_serial_cb_t cb);
|
||||
|
||||
/**
|
||||
* @brief Send a BLE Notification on the Flashing characteristic.
|
||||
* Used to send ACK/ERR responses back to the Webapp.
|
||||
*/
|
||||
void ble_service_send_notify_flashing(uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Send a BLE Notification on the Serial characteristic.
|
||||
* Used to forward Arduino serial output to the Webapp.
|
||||
*/
|
||||
void ble_service_send_notify_serial(uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Check if a BLE central is currently connected.
|
||||
* @return true if connected
|
||||
*/
|
||||
bool ble_service_is_connected(void);
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
#include "checksum.h"
|
||||
|
||||
static uint32_t crc32_table[256];
|
||||
static int table_initialized = 0;
|
||||
|
||||
static void crc32_init_table(void)
|
||||
{
|
||||
for (uint32_t i = 0; i < 256; i++) {
|
||||
uint32_t c = i;
|
||||
for (int j = 0; j < 8; j++) {
|
||||
c = (c & 1) ? (0xEDB88320 ^ (c >> 1)) : (c >> 1);
|
||||
}
|
||||
crc32_table[i] = c;
|
||||
}
|
||||
table_initialized = 1;
|
||||
}
|
||||
|
||||
uint32_t checksum_crc32(const uint8_t *data, size_t len)
|
||||
{
|
||||
if (!table_initialized) {
|
||||
crc32_init_table();
|
||||
}
|
||||
if (!data || len == 0) return 0;
|
||||
|
||||
uint32_t crc = 0xFFFFFFFF;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
crc = crc32_table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8);
|
||||
}
|
||||
return crc ^ 0xFFFFFFFF;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* @file checksum.h
|
||||
* @brief CRC32 checksum with pre-computed lookup table.
|
||||
*
|
||||
* Uses the standard IEEE 802.3 polynomial (0xEDB88320).
|
||||
* Table is initialised on first call, cached for subsequent calls.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* @brief Compute CRC32 over a memory buffer.
|
||||
* @param data Pointer to input bytes
|
||||
* @param len Number of bytes
|
||||
* @return CRC32 value (reflected, final XOR 0xFFFFFFFF)
|
||||
*/
|
||||
uint32_t checksum_crc32(const uint8_t *data, size_t len);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
dependencies:
|
||||
cherry-embedded/cherryusb: "^1.6.1"
|
||||
idf: ">=4.4"
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* @file state_machine.h
|
||||
* @brief Finite state machine orchestrating the deploy lifecycle.
|
||||
*
|
||||
* States:
|
||||
* IDLE -> RECEIVING -> VERIFYING -> FLASHING -> SERIAL_BRIDGE
|
||||
* |
|
||||
* ERROR_CHECKSUM <--- VERIFY_FAIL |
|
||||
* ERROR_TARGET <--- FLASH_FAIL / USB_DISCONNECT <----+
|
||||
*
|
||||
* Transitions are driven by events from BLE, USB, and the button.
|
||||
*
|
||||
* Flashing runs in a dedicated flasher_task (created in state_machine_init),
|
||||
* so the BLE host thread is never blocked for the ~8s flash duration. The
|
||||
* VERIFYING -> FLASHING transition hands off via flasher_sem; the flasher
|
||||
* task posts EVENT_FLASH_OK / EVENT_FLASH_FAIL back into the SM when done.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/** System states. */
|
||||
typedef enum {
|
||||
STATE_IDLE, /**< Waiting for BLE INIT command */
|
||||
STATE_RECEIVING, /**< Receiving binary chunks via BLE */
|
||||
STATE_VERIFYING, /**< CRC32 verification of accumulated buffer */
|
||||
STATE_FLASHING, /**< Programming Arduino via STK500v1 */
|
||||
STATE_SERIAL_BRIDGE, /**< Transparent CDC <-> BLE bridge active */
|
||||
STATE_ERROR_TARGET, /**< USB/STK500 failure — LED red, wait for Retry */
|
||||
STATE_ERROR_CHECKSUM /**< CRC mismatch — LED red blink, wait for Retry */
|
||||
} deployer_state_t;
|
||||
|
||||
/** Events that trigger state transitions. */
|
||||
typedef enum {
|
||||
EVENT_BLE_INIT, /**< Received INIT command from BLE */
|
||||
EVENT_BLE_DATA, /**< Received DATA chunk (internal) */
|
||||
EVENT_BLE_END, /**< Received END command */
|
||||
EVENT_VERIFY_OK, /**< CRC32 verification passed */
|
||||
EVENT_VERIFY_FAIL, /**< CRC32 verification failed */
|
||||
EVENT_FLASH_OK, /**< STK500v1 flashing succeeded (posted by flasher task) */
|
||||
EVENT_FLASH_FAIL, /**< STK500v1 flashing failed (posted by flasher task) */
|
||||
EVENT_BUTTON_RETRY, /**< Physical Retry button pressed */
|
||||
EVENT_BLE_DISCONNECT, /**< BLE link lost */
|
||||
EVENT_USB_DISCONNECT /**< Arduino USB disconnected */
|
||||
} sm_event_t;
|
||||
|
||||
void state_machine_init(void);
|
||||
void state_machine_process_event(sm_event_t event, void *data);
|
||||
deployer_state_t state_machine_get_current(void);
|
||||
const char *state_machine_get_state_name(void);
|
||||
void state_machine_tick(void);
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "stk500v1.h"
|
||||
#include "usb_host.h"
|
||||
|
||||
static const char *TAG = "STK500";
|
||||
|
||||
/* Read exactly resp_len bytes within timeout_ms. Returns true on success. */
|
||||
static bool read_exact(uint8_t *buf, size_t resp_len, uint32_t timeout_ms)
|
||||
{
|
||||
size_t got = 0;
|
||||
int64_t deadline = esp_timer_get_time() + (int64_t)timeout_ms * 1000;
|
||||
|
||||
while (got < resp_len) {
|
||||
int64_t remaining_us = deadline - esp_timer_get_time();
|
||||
if (remaining_us <= 0) {
|
||||
ESP_LOGE(TAG, "read_exact timeout: got %d/%d", (int)got, (int)resp_len);
|
||||
return false;
|
||||
}
|
||||
uint32_t chunk_to = (uint32_t)((remaining_us + 999) / 1000);
|
||||
if (chunk_to == 0) chunk_to = 1;
|
||||
|
||||
int n = usb_host_read_cdc(buf + got, resp_len - got, chunk_to);
|
||||
if (n < 0) {
|
||||
/* timeout or error on this chunk — keep trying until deadline */
|
||||
continue;
|
||||
}
|
||||
if (n > 0) {
|
||||
got += (size_t)n;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Send a command (already includes CRC_EOP), then expect STK_INSYNC and
|
||||
* optional payload + STK_OK. strict_ok=true requires the trailing STK_OK. */
|
||||
static bool send_and_expect(const uint8_t *cmd, size_t cmd_len,
|
||||
uint8_t *resp, size_t resp_len,
|
||||
uint32_t timeout_ms, bool strict_ok)
|
||||
{
|
||||
if (!usb_host_write_cdc(cmd, cmd_len)) {
|
||||
ESP_LOGE(TAG, "send failed (%d bytes)", (int)cmd_len);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t insync = 0;
|
||||
if (!read_exact(&insync, 1, timeout_ms)) {
|
||||
ESP_LOGE(TAG, "no INSYNC (got 0x%02X)", insync);
|
||||
return false;
|
||||
}
|
||||
if (insync == STK_NOSYNC) {
|
||||
ESP_LOGE(TAG, "optiboot replied NOSYNC");
|
||||
return false;
|
||||
}
|
||||
if (insync != STK_INSYNC) {
|
||||
ESP_LOGE(TAG, "expected INSYNC 0x14, got 0x%02X", insync);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resp && resp_len > 0) {
|
||||
if (!read_exact(resp, resp_len, timeout_ms)) {
|
||||
ESP_LOGE(TAG, "payload read failed (%d bytes)", (int)resp_len);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (strict_ok) {
|
||||
uint8_t ok = 0;
|
||||
if (!read_exact(&ok, 1, timeout_ms)) {
|
||||
ESP_LOGE(TAG, "no STK_OK");
|
||||
return false;
|
||||
}
|
||||
if (ok != STK_OK) {
|
||||
ESP_LOGE(TAG, "expected OK 0x10, got 0x%02X", ok);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool stk500v1_init(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "STK500v1 layer initialised (optiboot / ATmega328P)");
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cmd_get_sync(void)
|
||||
{
|
||||
uint8_t cmd[] = { STK_GET_SYNC, STK_CRC_EOP };
|
||||
|
||||
for (int attempt = 0; attempt < STK_SYNC_RETRIES; attempt++) {
|
||||
if (send_and_expect(cmd, sizeof(cmd), NULL, 0,
|
||||
STK_CMD_TIMEOUT_MS, true)) {
|
||||
if (attempt > 0) {
|
||||
ESP_LOGI(TAG, "get_sync OK after %d retries", attempt);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "get_sync OK");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/* Drain any stray bytes before retrying. */
|
||||
uint8_t drain[16];
|
||||
usb_host_read_cdc(drain, sizeof(drain), 10);
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
}
|
||||
ESP_LOGE(TAG, "get_sync failed after %d attempts", STK_SYNC_RETRIES);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool cmd_get_signature(uint8_t sig[3])
|
||||
{
|
||||
uint8_t cmd[] = { STK_READ_SIGN, STK_CRC_EOP };
|
||||
uint8_t resp[3] = {0};
|
||||
|
||||
if (!send_and_expect(cmd, sizeof(cmd), resp, sizeof(resp),
|
||||
STK_CMD_TIMEOUT_MS, true)) {
|
||||
return false;
|
||||
}
|
||||
sig[0] = resp[0];
|
||||
sig[1] = resp[1];
|
||||
sig[2] = resp[2];
|
||||
ESP_LOGI(TAG, "signature: %02X %02X %02X", sig[0], sig[1], sig[2]);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cmd_enter_progmode(void)
|
||||
{
|
||||
uint8_t cmd[] = { STK_ENTER_PROGMODE, STK_CRC_EOP };
|
||||
return send_and_expect(cmd, sizeof(cmd), NULL, 0,
|
||||
STK_CMD_TIMEOUT_MS, true);
|
||||
}
|
||||
|
||||
static bool cmd_load_address(uint16_t word_addr)
|
||||
{
|
||||
uint8_t cmd[] = {
|
||||
STK_LOAD_ADDRESS,
|
||||
(uint8_t)(word_addr & 0xFF),
|
||||
(uint8_t)((word_addr >> 8) & 0xFF),
|
||||
STK_CRC_EOP
|
||||
};
|
||||
return send_and_expect(cmd, sizeof(cmd), NULL, 0,
|
||||
STK_CMD_TIMEOUT_MS, true);
|
||||
}
|
||||
|
||||
static bool cmd_prog_page(const uint8_t *data, uint16_t len)
|
||||
{
|
||||
/* Header: 0x64, len_hi, len_lo, memtype 'F'(0x46). Then data, then EOP. */
|
||||
uint8_t header[4] = {
|
||||
STK_PROG_PAGE,
|
||||
(uint8_t)((len >> 8) & 0xFF),
|
||||
(uint8_t)(len & 0xFF),
|
||||
0x46 /* 'F' = flash */
|
||||
};
|
||||
|
||||
/* Send header + data + EOP as one logical transfer. CherryUSB write is
|
||||
* a single URB, so we build a contiguous buffer. */
|
||||
static uint8_t pkt[4 + ATMEGA328P_PAGE_SIZE + 1];
|
||||
if (len > ATMEGA328P_PAGE_SIZE) {
|
||||
ESP_LOGE(TAG, "prog_page len %d exceeds page %d", len, ATMEGA328P_PAGE_SIZE);
|
||||
return false;
|
||||
}
|
||||
memcpy(pkt, header, 4);
|
||||
memcpy(pkt + 4, data, len);
|
||||
pkt[4 + len] = STK_CRC_EOP;
|
||||
|
||||
return send_and_expect(pkt, 4 + len + 1, NULL, 0,
|
||||
STK_PAGE_TIMEOUT_MS, true);
|
||||
}
|
||||
|
||||
static bool cmd_leave_progmode(void)
|
||||
{
|
||||
uint8_t cmd[] = { STK_LEAVE_PROGMODE, STK_CRC_EOP };
|
||||
/* optiboot shortens WDT and resets; STK_OK may be absent. Lenient. */
|
||||
bool ok = send_and_expect(cmd, sizeof(cmd), NULL, 0,
|
||||
STK_LEAVE_TIMEOUT_MS, false);
|
||||
if (!ok) {
|
||||
ESP_LOGW(TAG, "leave_progmode did not reply (expected on optiboot)");
|
||||
/* Treat as success — optiboot intentionally resets. */
|
||||
ok = true;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool stk500v1_flash_buffer(const uint8_t *buffer, size_t size, uint16_t page_size)
|
||||
{
|
||||
if (!buffer || size == 0) {
|
||||
ESP_LOGE(TAG, "flash_buffer: null/empty buffer");
|
||||
return false;
|
||||
}
|
||||
if (page_size == 0) {
|
||||
ESP_LOGE(TAG, "flash_buffer: page_size=0");
|
||||
return false;
|
||||
}
|
||||
if (size > ATMEGA328P_FLASH_SIZE) {
|
||||
ESP_LOGE(TAG, "flash_buffer: size %d exceeds flash %d",
|
||||
(int)size, ATMEGA328P_FLASH_SIZE);
|
||||
return false;
|
||||
}
|
||||
if (page_size != ATMEGA328P_PAGE_SIZE) {
|
||||
ESP_LOGW(TAG, "page_size %d != expected %d — using provided",
|
||||
page_size, ATMEGA328P_PAGE_SIZE);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Starting flash: %d bytes, page %d", (int)size, page_size);
|
||||
|
||||
/* 1. Auto-reset Arduino to (re)enter optiboot. */
|
||||
usb_host_reset_arduino();
|
||||
|
||||
/* 2. Get sync (retry within optiboot ~1s window). */
|
||||
if (!cmd_get_sync()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 3. Read & validate signature. */
|
||||
uint8_t sig[3] = {0};
|
||||
if (!cmd_get_signature(sig)) {
|
||||
return false;
|
||||
}
|
||||
if (sig[0] != ATMEGA328P_SIG_0 || sig[1] != ATMEGA328P_SIG_1 ||
|
||||
sig[2] != ATMEGA328P_SIG_2) {
|
||||
ESP_LOGE(TAG, "signature mismatch: got %02X %02X %02X, want %02X %02X %02X",
|
||||
sig[0], sig[1], sig[2],
|
||||
ATMEGA328P_SIG_0, ATMEGA328P_SIG_1, ATMEGA328P_SIG_2);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 4. Enter programming mode. */
|
||||
if (!cmd_enter_progmode()) {
|
||||
ESP_LOGE(TAG, "enter_progmode failed");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGI(TAG, "entered programming mode");
|
||||
|
||||
/* 5. Program pages. ATmega328P uses word addresses (byte/2). */
|
||||
uint16_t pages = (uint16_t)((size + page_size - 1) / page_size);
|
||||
for (uint16_t page = 0; page < pages; page++) {
|
||||
uint16_t byte_addr = (uint16_t)(page * page_size);
|
||||
uint16_t word_addr = byte_addr / 2;
|
||||
uint16_t remaining = (uint16_t)(size - byte_addr);
|
||||
uint16_t this_len = (remaining > page_size) ? page_size : remaining;
|
||||
|
||||
if (!cmd_load_address(word_addr)) {
|
||||
ESP_LOGE(TAG, "load_address failed at page %d (word 0x%04X)",
|
||||
page + 1, word_addr);
|
||||
cmd_leave_progmode();
|
||||
return false;
|
||||
}
|
||||
if (!cmd_prog_page(buffer + byte_addr, this_len)) {
|
||||
ESP_LOGE(TAG, "prog_page failed at page %d/%d (byte 0x%04X, %d bytes)",
|
||||
page + 1, pages, byte_addr, this_len);
|
||||
cmd_leave_progmode();
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((page + 1) % 16 == 0 || page + 1 == pages) {
|
||||
ESP_LOGI(TAG, "flashing page %d/%d", page + 1, pages);
|
||||
}
|
||||
/* Yield to keep BLE host stack alive. */
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
|
||||
/* 6. Leave programming mode. */
|
||||
cmd_leave_progmode();
|
||||
|
||||
ESP_LOGI(TAG, "Flash complete: %d pages written", pages);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* @file stk500v1.h
|
||||
* @brief STK500v1 protocol implementation for flashing ATmega328P via optiboot.
|
||||
*
|
||||
* Communicates with Arduino's optiboot bootloader over USB CDC using the
|
||||
* STK500v1 command set (verified against avrdude stk500.c + optiboot.c):
|
||||
* - get_sync (0x30 0x20) -> 0x14 0x10
|
||||
* - get_signature (0x75 0x20) -> 0x14 1E 95 0F 0x10
|
||||
* - enter_progmode (0x50 0x20) -> 0x14 0x10
|
||||
* - load_address (0x55 lo hi 0x20) -> 0x14 0x10 (word address, LE)
|
||||
* - prog_page (0x64 len_hi len_lo 'F' data[] 0x20) -> 0x14 0x10
|
||||
* - leave_progmode (0x51 0x20) -> 0x14 (optiboot WDT-resets, OK optional)
|
||||
*
|
||||
* Every command is terminated with CRC_EOP=0x20. optiboot replies with
|
||||
* STK_INSYNC=0x14 immediately, then payload, then STK_OK=0x10.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/* STK500v1 command bytes (from Atmel AVR061 command.h, matches optiboot) */
|
||||
#define STK_CRC_EOP 0x20 /* End-of-packet sentinel */
|
||||
#define STK_GET_SYNC 0x30 /* Echos sync, establishes comms */
|
||||
#define STK_ENTER_PROGMODE 0x50 /* Enter programming mode */
|
||||
#define STK_LEAVE_PROGMODE 0x51 /* Leave programming mode */
|
||||
#define STK_LOAD_ADDRESS 0x55 /* Load address (word addr, LE) */
|
||||
#define STK_PROG_PAGE 0x64 /* Program flash page */
|
||||
#define STK_READ_PAGE 0x74 /* Read flash page (verify) */
|
||||
#define STK_READ_SIGN 0x75 /* Read device signature bytes */
|
||||
|
||||
/* STK500v1 response bytes */
|
||||
#define STK_INSYNC 0x14 /* Command accepted */
|
||||
#define STK_OK 0x10 /* Command completed */
|
||||
#define STK_NOSYNC 0x15 /* Lost sync */
|
||||
|
||||
/* ATmega328P device constants */
|
||||
#define ATMEGA328P_SIG_0 0x1E
|
||||
#define ATMEGA328P_SIG_1 0x95
|
||||
#define ATMEGA328P_SIG_2 0x0F
|
||||
#define ATMEGA328P_FLASH_SIZE 32768 /* bytes */
|
||||
#define ATMEGA328P_PAGE_SIZE 128 /* bytes */
|
||||
|
||||
/* Timing / retries */
|
||||
#define STK_SYNC_RETRIES 10 /* get_sync attempts (optiboot window ~1s) */
|
||||
#define STK_CMD_TIMEOUT_MS 200 /* per-command response timeout */
|
||||
#define STK_PAGE_TIMEOUT_MS 500 /* prog_page timeout (page write ~4ms) */
|
||||
#define STK_LEAVE_TIMEOUT_MS 100 /* leave_progmode (OK may be absent) */
|
||||
|
||||
/**
|
||||
* @brief Initialise the STK500v1 layer (no USB work — just logging).
|
||||
* @return true always
|
||||
*/
|
||||
bool stk500v1_init(void);
|
||||
|
||||
/**
|
||||
* @brief Program a raw binary image into ATmega328P flash via optiboot STK500v1.
|
||||
*
|
||||
* Performs: auto-reset (DTR pulse) -> get_sync -> get_signature -> enter_progmode
|
||||
* -> loop (load_address + prog_page) -> leave_progmode.
|
||||
* Blocks the calling task for the full flash duration (~8s for 32KB).
|
||||
* The caller MUST have claimed USB RX first (usb_host_rx_claim()).
|
||||
*
|
||||
* @param buffer Raw binary flash image (must be <= 32768 bytes)
|
||||
* @param size Size in bytes
|
||||
* @param page_size Page size (typically 128 for ATmega328P)
|
||||
* @return true if all pages written & signature verified
|
||||
*/
|
||||
bool stk500v1_flash_buffer(const uint8_t *buffer, size_t size, uint16_t page_size);
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "usbh_core.h"
|
||||
#include "usbh_serial.h"
|
||||
#include "usb_host.h"
|
||||
|
||||
static const char *TAG = "USB_HOST";
|
||||
|
||||
static usb_data_cb_t serial_callback = NULL;
|
||||
static bool arduino_connected_flag = false;
|
||||
static bool initialized = false;
|
||||
static bool rx_claimed = false;
|
||||
static struct usbh_serial *serial_dev = NULL;
|
||||
static TaskHandle_t usb_monitor_task_handle = NULL;
|
||||
static TaskHandle_t usb_rx_task_handle = NULL;
|
||||
|
||||
/* Default termios (serial bridge): blocking RX, 115200 8N1. */
|
||||
static struct usbh_serial_termios make_termios(uint32_t rx_timeout)
|
||||
{
|
||||
struct usbh_serial_termios t = {
|
||||
.baudrate = 115200,
|
||||
.databits = 8,
|
||||
.parity = 0,
|
||||
.stopbits = 0,
|
||||
.rtscts = false,
|
||||
.rx_timeout = rx_timeout,
|
||||
};
|
||||
return t;
|
||||
}
|
||||
|
||||
static void usb_rx_task(void *arg)
|
||||
{
|
||||
uint8_t buf[512];
|
||||
int ret;
|
||||
|
||||
while (1) {
|
||||
/* If suspended by rx_claim, just idle (suspension handled by VTaskSuspend). */
|
||||
if (serial_dev && arduino_connected_flag && !rx_claimed) {
|
||||
ret = usbh_serial_read(serial_dev, buf, sizeof(buf));
|
||||
if (ret > 0) {
|
||||
if (serial_callback) {
|
||||
serial_callback(buf, ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
vTaskDelay(1);
|
||||
}
|
||||
}
|
||||
|
||||
static void usb_monitor_task(void *arg)
|
||||
{
|
||||
struct usbh_serial *dev;
|
||||
|
||||
while (1) {
|
||||
if (!arduino_connected_flag) {
|
||||
dev = usbh_serial_open("/dev/ttyACM0", USBH_SERIAL_O_RDWR);
|
||||
if (!dev) {
|
||||
dev = usbh_serial_open("/dev/ttyUSB0", USBH_SERIAL_O_RDWR);
|
||||
}
|
||||
if (dev) {
|
||||
serial_dev = dev;
|
||||
arduino_connected_flag = true;
|
||||
|
||||
struct usbh_serial_termios t = make_termios(0);
|
||||
usbh_serial_control(dev, USBH_SERIAL_CMD_SET_ATTR, &t);
|
||||
/* SET_ATTR already drives DTR|RTS high internally (see
|
||||
* usbh_serial.c SET_ATTR handler). Do NOT issue TIOCMSET
|
||||
* with value-as-pointer — that derefs an invalid address. */
|
||||
|
||||
ESP_LOGI(TAG, "Arduino connected via CherryUSB");
|
||||
}
|
||||
} else {
|
||||
int ret = usbh_serial_write(serial_dev, NULL, 0);
|
||||
if (ret < 0) {
|
||||
ESP_LOGI(TAG, "Arduino disconnected");
|
||||
arduino_connected_flag = false;
|
||||
usbh_serial_close(serial_dev);
|
||||
serial_dev = NULL;
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
|
||||
bool usb_host_init(void)
|
||||
{
|
||||
if (initialized) return true;
|
||||
|
||||
esp_err_t ret = usbh_initialize(0, ESP_USB_FS0_BASE, NULL);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "CherryUSB init failed: %d", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
xTaskCreate(usb_monitor_task, "usb_mon", 4096, NULL, 3, &usb_monitor_task_handle);
|
||||
xTaskCreate(usb_rx_task, "usb_rx", 2560, NULL, 4, &usb_rx_task_handle);
|
||||
|
||||
initialized = true;
|
||||
ESP_LOGI(TAG, "USB Host initialized (CherryUSB)");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool usb_host_arduino_connected(void)
|
||||
{
|
||||
return arduino_connected_flag && serial_dev != NULL;
|
||||
}
|
||||
|
||||
bool usb_host_write_cdc(const uint8_t *data, size_t len)
|
||||
{
|
||||
if (!serial_dev) return false;
|
||||
|
||||
int ret = usbh_serial_write(serial_dev, data, len);
|
||||
if (ret < 0) {
|
||||
ESP_LOGE(TAG, "Serial write failed: %d", ret);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int usb_host_read_cdc(uint8_t *buf, size_t len, uint32_t timeout_ms)
|
||||
{
|
||||
if (!serial_dev || !arduino_connected_flag) {
|
||||
return -1;
|
||||
}
|
||||
/* CherryUSB uses serial_dev->rx_timeout_ms for the sem_take timeout
|
||||
* (see usbh_serial.c:461). Setting it inline is the documented field. */
|
||||
serial_dev->rx_timeout_ms = timeout_ms;
|
||||
int ret = usbh_serial_read(serial_dev, buf, len);
|
||||
return ret; /* >=0 bytes, <0 on error/timeout */
|
||||
}
|
||||
|
||||
void usb_host_set_serial_callback(usb_data_cb_t cb)
|
||||
{
|
||||
serial_callback = cb;
|
||||
}
|
||||
|
||||
void usb_host_rx_claim(void)
|
||||
{
|
||||
if (!initialized || rx_claimed) return;
|
||||
|
||||
/* Stop the rx/monitor tasks so they don't drain the CDC ringbuffer. */
|
||||
if (usb_rx_task_handle) vTaskSuspend(usb_rx_task_handle);
|
||||
if (usb_monitor_task_handle) vTaskSuspend(usb_monitor_task_handle);
|
||||
|
||||
if (serial_dev) {
|
||||
/* Reconfigure with bounded rx_timeout so usb_host_read_cdc returns
|
||||
* on timeout. SET_ATTR also kills+resubmits the IN URB and resets
|
||||
* the ringbuffer, clearing any stale serial data. */
|
||||
struct usbh_serial_termios t = make_termios(50);
|
||||
usbh_serial_control(serial_dev, USBH_SERIAL_CMD_SET_ATTR, &t);
|
||||
}
|
||||
rx_claimed = true;
|
||||
ESP_LOGI(TAG, "RX claimed for STK500");
|
||||
}
|
||||
|
||||
void usb_host_rx_release(void)
|
||||
{
|
||||
if (!initialized || !rx_claimed) return;
|
||||
|
||||
if (serial_dev) {
|
||||
/* Restore blocking RX (rx_timeout=0 = forever) for serial bridge. */
|
||||
struct usbh_serial_termios t = make_termios(0);
|
||||
usbh_serial_control(serial_dev, USBH_SERIAL_CMD_SET_ATTR, &t);
|
||||
}
|
||||
rx_claimed = false;
|
||||
|
||||
if (usb_rx_task_handle) vTaskResume(usb_rx_task_handle);
|
||||
if (usb_monitor_task_handle) vTaskResume(usb_monitor_task_handle);
|
||||
ESP_LOGI(TAG, "RX released back to serial bridge");
|
||||
}
|
||||
|
||||
void usb_host_reset_arduino(void)
|
||||
{
|
||||
if (!serial_dev || !arduino_connected_flag) {
|
||||
ESP_LOGW(TAG, "reset_arduino: no device — skipping DTR pulse");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Drive DTR+RTS low to assert RESET (Arduino autoreset circuit).
|
||||
* TIOCMSET expects a pointer to uint32_t flags — NEVER pass flags
|
||||
* cast directly as the pointer (that was bug B10, a NULL+small deref). */
|
||||
uint32_t flags_low = 0;
|
||||
usbh_serial_control(serial_dev, USBH_SERIAL_CMD_TIOCMSET, &flags_low);
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
|
||||
uint32_t flags_high = USBH_SERIAL_TIOCM_DTR | USBH_SERIAL_TIOCM_RTS;
|
||||
usbh_serial_control(serial_dev, USBH_SERIAL_CMD_TIOCMSET, &flags_high);
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
|
||||
ESP_LOGI(TAG, "Arduino DTR pulse sent (autoreset)");
|
||||
}
|
||||
|
||||
void usb_host_deinit(void)
|
||||
{
|
||||
if (!initialized) return;
|
||||
|
||||
initialized = false;
|
||||
arduino_connected_flag = false;
|
||||
rx_claimed = false;
|
||||
|
||||
if (usb_monitor_task_handle) {
|
||||
vTaskDelete(usb_monitor_task_handle);
|
||||
usb_monitor_task_handle = NULL;
|
||||
}
|
||||
if (usb_rx_task_handle) {
|
||||
vTaskDelete(usb_rx_task_handle);
|
||||
usb_rx_task_handle = NULL;
|
||||
}
|
||||
if (serial_dev) {
|
||||
usbh_serial_close(serial_dev);
|
||||
serial_dev = NULL;
|
||||
}
|
||||
usbh_deinitialize(0);
|
||||
|
||||
ESP_LOGI(TAG, "USB Host deinitialized");
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* @file usb_host.h
|
||||
* @brief USB Host CDC driver using CherryUSB.
|
||||
*
|
||||
* Manages enumeration of Arduino Uno/Nano as a CDC serial device,
|
||||
* providing read/write access for STK500v1 flashing and serial bridge.
|
||||
*
|
||||
* RX routing: by default the rx_task feeds the serial-bridge callback.
|
||||
* During flashing, usb_host_rx_claim() suspends the rx/monitor tasks so
|
||||
* STK500v1 can read CDC responses exclusively via usb_host_read_cdc().
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* @brief Callback invoked when data is received from Arduino via USB CDC
|
||||
* while RX is NOT claimed (serial bridge mode).
|
||||
* @param data Received bytes
|
||||
* @param len Number of bytes
|
||||
*/
|
||||
typedef void (*usb_data_cb_t)(uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Initialise CherryUSB Host driver and start monitor/rx tasks.
|
||||
* @return true on success
|
||||
*/
|
||||
bool usb_host_init(void);
|
||||
|
||||
/**
|
||||
* @brief Check if an Arduino CDC device is currently enumerated.
|
||||
* @return true if connected
|
||||
*/
|
||||
bool usb_host_arduino_connected(void);
|
||||
|
||||
/**
|
||||
* @brief Write data to the Arduino via USB CDC (blocking until TX done).
|
||||
* @return true if write succeeded
|
||||
*/
|
||||
bool usb_host_write_cdc(const uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Read up to len bytes from Arduino CDC within timeout.
|
||||
*
|
||||
* Only valid while RX is claimed (i.e. during flashing). Reads from the
|
||||
* CherryUSB ringbuffer; blocks up to timeout_ms waiting for data.
|
||||
*
|
||||
* @param buf Destination buffer
|
||||
* @param len Max bytes to read
|
||||
* @param timeout_ms Max wait time
|
||||
* @return number of bytes read (>=0), or negative on error/timeout
|
||||
*/
|
||||
int usb_host_read_cdc(uint8_t *buf, size_t len, uint32_t timeout_ms);
|
||||
|
||||
/**
|
||||
* @brief Register callback for incoming CDC serial data (serial bridge).
|
||||
*/
|
||||
void usb_host_set_serial_callback(usb_data_cb_t cb);
|
||||
|
||||
/**
|
||||
* @brief Claim USB CDC RX for STK500v1 flashing.
|
||||
*
|
||||
* Suspends the rx_task and monitor_task so STK500v1 can exclusively read
|
||||
* responses via usb_host_read_cdc(). Reconfigures the CDC port with a
|
||||
* bounded rx_timeout so reads actually return on timeout.
|
||||
*/
|
||||
void usb_host_rx_claim(void);
|
||||
|
||||
/**
|
||||
* @brief Release USB CDC RX back to serial bridge mode.
|
||||
*
|
||||
* Resumes rx_task and monitor_task and restores rx_timeout=0 (blocking).
|
||||
*/
|
||||
void usb_host_rx_release(void);
|
||||
|
||||
/**
|
||||
* @brief Pulse DTR low then high to reset Arduino into optiboot.
|
||||
*
|
||||
* Mirrors avrdude's Arduino autoreset: DTR/RTS low ~1ms, then high,
|
||||
* wait 50ms for optiboot to enter. Idempotent if no device present.
|
||||
*/
|
||||
void usb_host_reset_arduino(void);
|
||||
|
||||
/**
|
||||
* @brief Deinitialise USB Host and release resources.
|
||||
*/
|
||||
void usb_host_deinit(void);
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Name, Type, SubType, Offset, Size
|
||||
nvs, data, nvs, 0x9000, 0x6000
|
||||
otadata, data, ota, 0xf000, 0x2000
|
||||
ota_0, app, ota_0, 0x20000, 0x300000
|
||||
ota_1, app, ota_1, 0x320000,0x300000
|
||||
storage, data, spiffs, 0x620000,0x9E0000
|
||||
|
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,50 @@
|
|||
# Velxio BLE Deployer - ESP32-S3 N16R8
|
||||
# ESP-IDF v6.0
|
||||
# Native USB OTG (GPIO19/20) = USB Host → Arduino
|
||||
# Debug log via UART0 (USB-to-UART bridge devkit)
|
||||
|
||||
# FreeRTOS
|
||||
CONFIG_FREERTOS_HZ=100
|
||||
|
||||
# NimBLE
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=y
|
||||
CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME="Velxio-Deployer"
|
||||
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
|
||||
CONFIG_BT_NIMBLE_MAX_BONDS=1
|
||||
CONFIG_BT_NIMBLE_HS_STOP_TIMEOUT_MS=5000
|
||||
CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=255
|
||||
|
||||
# PSRAM (octal, N16R8)
|
||||
CONFIG_SPIRAM=y
|
||||
CONFIG_SPIRAM_MODE_OCT=y
|
||||
CONFIG_SPIRAM_TYPE_AUTO=y
|
||||
CONFIG_SPIRAM_USE_CAPS_ALLOC=y
|
||||
|
||||
# Partition Table
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
||||
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
|
||||
|
||||
# Compiler Optimizations
|
||||
CONFIG_COMPILER_OPTIMIZATION_PERF=y
|
||||
|
||||
# Flash Size (16 MB untuk ESP32-S3 N16R8)
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
|
||||
|
||||
# Logging
|
||||
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
||||
|
||||
# Serial console via UART0 (bukan USB-JTAG, karena USB native dipakai Host)
|
||||
CONFIG_ESP_CONSOLE_UART_DEFAULT=y
|
||||
|
||||
# CherryUSB Host (native USB OTG S3)
|
||||
CONFIG_CHERRYUSB=y
|
||||
CONFIG_CHERRYUSB_HOST=y
|
||||
# Serial drivers — enable all for max Arduino clone compatibility
|
||||
CONFIG_CHERRYUSB_HOST_CDC_ACM=y
|
||||
CONFIG_CHERRYUSB_HOST_CH34X=y
|
||||
CONFIG_CHERRYUSB_HOST_FTDI=y
|
||||
CONFIG_CHERRYUSB_HOST_CP210X=y
|
||||
CONFIG_CHERRYUSB_HOST_PL2303=y
|
||||
Loading…
Reference in New Issue