fix: END ACK regression — readPoll pintar + INIT_ACK_INDEX 0xFFFE + binary_parser.c git track

Akar masalah:
- 4a6ef8c menambah binary_parser_reset() di handler re-deploy SERIAL_BRIDGE
  yang menghapus expected_total_crc setelah INIT — menyebabkan CRC mismatch
  di END. Source 1d3a489 sudah menghapus reset-nya, tapi device butuh rebuild.
- cad90eb merutekan 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:
a2nr 2026-07-01 05:10:38 +07:00
parent d146e445d6
commit c20b2d5a8e
20 changed files with 5875 additions and 26 deletions

View File

@ -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;
})();

View File

@ -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;

View File

@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(velxio-deployer)

View File

@ -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 |

View File

@ -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

View File

@ -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
.
)

View File

@ -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;
}

View File

@ -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);

View File

@ -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);

View File

@ -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;
}

View File

@ -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);

View File

@ -0,0 +1,3 @@
dependencies:
cherry-embedded/cherryusb: "^1.6.1"
idf: ">=4.4"

View File

@ -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);

View File

@ -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;
}

View File

@ -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);

View File

@ -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");
}

View File

@ -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);

View File

@ -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
1 # Name Type SubType Offset Size
2 nvs data nvs 0x9000 0x6000
3 otadata data ota 0xf000 0x2000
4 ota_0 app ota_0 0x20000 0x300000
5 ota_1 app ota_1 0x320000 0x300000
6 storage data spiffs 0x620000 0x9E0000

File diff suppressed because it is too large Load Diff

View File

@ -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