fix: re-deploy crash + readValue poll for INIT/DATA
Firmware (state_machine.c): - Add EVENT_BLE_INIT handler to SERIAL_BRIDGE state -> transition back to RECEIVING for re-deploy - Add EVENT_BLE_INIT handler to ERROR states -> allows re-deploy from error without pressing Retry Frontend (ble-deployer.ts): - Add readValue() state poll for INIT and DATA commands (same Android notify idle-drop bug affects these too) - INIT success: firmware state >= 1 (RECEIVING+) - DATA success: firmware state == 1 (RECEIVING) - Error on state >= 5
This commit is contained in:
parent
9918aafe41
commit
f6a04b81ba
|
|
@ -347,8 +347,67 @@ export class BLEHardwareDeployer {
|
|||
return;
|
||||
}
|
||||
|
||||
await ackPromise;
|
||||
console.log(`[BLE-CMD] ${cmdName} idx=${index}: ackPromise resolved OK`);
|
||||
/* 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). */
|
||||
if (cmd === CMD_INIT || cmd === CMD_DATA) {
|
||||
const readPoll = (async () => {
|
||||
const deadline = Date.now() + ackTimeout;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const dv = await this.withTimeout(
|
||||
this.flashingChar!.readValue(),
|
||||
3000,
|
||||
'readValue(state)'
|
||||
);
|
||||
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) {
|
||||
if (state >= 1 && state <= 4) {
|
||||
console.log(`[BLE-CMD] ${cmdName}: state=${state} — 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})`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!this.isConnected) {
|
||||
throw new Error('Koneksi BLE terputus');
|
||||
}
|
||||
console.log(`[BLE-CMD] ${cmdName}: readValue retry...`, e);
|
||||
}
|
||||
await new Promise<void>(r => setTimeout(r, 200));
|
||||
}
|
||||
console.log(`[BLE-CMD] ${cmdName}: readPoll deadline passed (${ackTimeout}ms)`);
|
||||
return 'timeout' as const;
|
||||
})();
|
||||
|
||||
const ackWithLabel = ackPromise.then(() => 'ack' as const);
|
||||
const result = await Promise.race([ackWithLabel, readPoll]);
|
||||
console.log(`[BLE-CMD] ${cmdName} idx=${index}: resolved via ${result}`);
|
||||
|
||||
if (result !== 'ack') {
|
||||
const entry = this.ackResolvers.get(index);
|
||||
if (entry) {
|
||||
clearTimeout(entry.timer);
|
||||
this.ackResolvers.delete(index);
|
||||
}
|
||||
}
|
||||
if (result === 'timeout') {
|
||||
throw new Error(`Timeout menunggu ACK untuk chunk ${index}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async sendCommandWithRetry(cmd: number, index: number, data: Uint8Array, chunkCRC: number): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,237 @@
|
|||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "state_machine.h"
|
||||
#include "binary_parser.h"
|
||||
#include "checksum.h"
|
||||
#include "usb_host.h"
|
||||
#include "stk500v1.h"
|
||||
#include "serial_bridge.h"
|
||||
#include "led_button.h"
|
||||
#include "ble_service.h"
|
||||
|
||||
static const char *TAG = "SM";
|
||||
|
||||
static deployer_state_t current_state = STATE_IDLE;
|
||||
static uint16_t stk500_page_size = 128;
|
||||
|
||||
/* Flasher task: blocks on flasher_sem, runs stk500v1 off the BLE thread. */
|
||||
static SemaphoreHandle_t flasher_sem = NULL;
|
||||
static TaskHandle_t flasher_task_handle = NULL;
|
||||
|
||||
/* Snapshot of the buffer to flash (valid only while in STATE_FLASHING). */
|
||||
static const uint8_t *flasher_buffer = NULL;
|
||||
static size_t flasher_size = 0;
|
||||
|
||||
static void flasher_task(void *arg)
|
||||
{
|
||||
while (1) {
|
||||
xSemaphoreTake(flasher_sem, portMAX_DELAY);
|
||||
|
||||
ESP_LOGI(TAG, "Flasher task started: %d bytes", (int)flasher_size);
|
||||
|
||||
/* Claim USB RX so STK500 can read CDC responses exclusively. */
|
||||
usb_host_rx_claim();
|
||||
|
||||
bool ok = stk500v1_flash_buffer(flasher_buffer, flasher_size,
|
||||
stk500_page_size);
|
||||
|
||||
usb_host_rx_release();
|
||||
|
||||
ESP_LOGI(TAG, "Flasher task done: %s", ok ? "OK" : "FAIL");
|
||||
state_machine_process_event(ok ? EVENT_FLASH_OK : EVENT_FLASH_FAIL,
|
||||
NULL);
|
||||
}
|
||||
}
|
||||
|
||||
void state_machine_init(void)
|
||||
{
|
||||
current_state = STATE_IDLE;
|
||||
esp_log_level_set(TAG, ESP_LOG_DEBUG);
|
||||
|
||||
flasher_sem = xSemaphoreCreateBinary();
|
||||
if (!flasher_sem) {
|
||||
ESP_LOGE(TAG, "Failed to create flasher_sem");
|
||||
return;
|
||||
}
|
||||
|
||||
BaseType_t r = xTaskCreate(flasher_task, "flasher", 8192, NULL, 4,
|
||||
&flasher_task_handle);
|
||||
if (r != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create flasher task");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "State machine initialized: IDLE (flasher task ready)");
|
||||
}
|
||||
|
||||
void state_machine_process_event(sm_event_t event, void *data)
|
||||
{
|
||||
ESP_LOGD(TAG, "Event: %d in state: %s", event, state_machine_get_state_name());
|
||||
|
||||
/* ACK unique index for END: 0xFFFF so the webapp can disambiguate from INIT (index=0). */
|
||||
static const uint16_t END_ACK_INDEX = 0xFFFF;
|
||||
|
||||
switch (current_state) {
|
||||
case STATE_IDLE:
|
||||
if (event == EVENT_BLE_INIT) {
|
||||
current_state = STATE_RECEIVING;
|
||||
led_set_pattern(LED_BLUE_BLINK);
|
||||
ESP_LOGI(TAG, "Transition: IDLE -> RECEIVING");
|
||||
}
|
||||
break;
|
||||
|
||||
case STATE_RECEIVING:
|
||||
if (event == EVENT_BLE_END) {
|
||||
current_state = STATE_VERIFYING;
|
||||
ESP_LOGI(TAG, "Transition: RECEIVING -> VERIFYING");
|
||||
state_machine_process_event(EVENT_VERIFY_OK, NULL);
|
||||
} else if (event == EVENT_BLE_INIT) {
|
||||
ESP_LOGE(TAG, "Unexpected INIT in RECEIVING state");
|
||||
} else if (event == EVENT_BLE_DISCONNECT) {
|
||||
ESP_LOGI(TAG, "BLE disconnect in RECEIVING, resetting to IDLE");
|
||||
binary_parser_reset();
|
||||
current_state = STATE_IDLE;
|
||||
led_set_pattern(LED_BLUE_BLINK);
|
||||
}
|
||||
break;
|
||||
|
||||
case STATE_VERIFYING:
|
||||
if (event == EVENT_VERIFY_OK) {
|
||||
uint32_t expected_crc = binary_parser_get_total_crc();
|
||||
uint8_t *buffer = binary_parser_get_buffer();
|
||||
size_t size = binary_parser_get_buffer_size();
|
||||
uint32_t actual_crc = checksum_crc32(buffer, size);
|
||||
|
||||
if (actual_crc == expected_crc) {
|
||||
ESP_LOGI(TAG, "CRC verification OK: 0x%08X", actual_crc);
|
||||
current_state = STATE_FLASHING;
|
||||
led_set_pattern(LED_BLUE);
|
||||
|
||||
/* Hand off to flasher task instead of blocking the BLE
|
||||
* thread. Snapshot buffer ptr/size — parser won't mutate
|
||||
* them until next INIT (which can't happen in FLASHING). */
|
||||
flasher_buffer = buffer;
|
||||
flasher_size = size;
|
||||
xSemaphoreGive(flasher_sem);
|
||||
ESP_LOGI(TAG, "Transition: VERIFYING -> FLASHING (flasher armed)");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "CRC verification FAILED: expected 0x%08X, got 0x%08X",
|
||||
expected_crc, actual_crc);
|
||||
state_machine_process_event(EVENT_VERIFY_FAIL, NULL);
|
||||
}
|
||||
} else if (event == EVENT_VERIFY_FAIL) {
|
||||
current_state = STATE_ERROR_CHECKSUM;
|
||||
led_set_pattern(LED_RED_BLINK);
|
||||
|
||||
uint8_t err[] = {CMD_ERR, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00};
|
||||
memcpy(err + 4, "CRC MISMATCH", 12);
|
||||
ble_service_send_notify_flashing(err, sizeof(err));
|
||||
ESP_LOGI(TAG, "Transition: VERIFYING -> ERROR_CHECKSUM");
|
||||
} else if (event == EVENT_BLE_DISCONNECT) {
|
||||
ESP_LOGI(TAG, "BLE disconnect in VERIFYING, resetting to IDLE");
|
||||
binary_parser_reset();
|
||||
current_state = STATE_IDLE;
|
||||
led_set_pattern(LED_BLUE_BLINK);
|
||||
}
|
||||
break;
|
||||
|
||||
case STATE_FLASHING:
|
||||
if (event == EVENT_FLASH_OK) {
|
||||
/* Flasher task completed successfully — send ACK to webapp. */
|
||||
current_state = STATE_SERIAL_BRIDGE;
|
||||
led_set_pattern(LED_GREEN);
|
||||
serial_bridge_start();
|
||||
|
||||
uint8_t ack[] = {CMD_ACK,
|
||||
END_ACK_INDEX & 0xFF,
|
||||
(END_ACK_INDEX >> 8) & 0xFF,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
ble_service_send_notify_flashing(ack, sizeof(ack));
|
||||
ESP_LOGI(TAG, "Transition: FLASHING -> SERIAL_BRIDGE");
|
||||
} else if (event == EVENT_FLASH_FAIL) {
|
||||
current_state = STATE_ERROR_TARGET;
|
||||
led_set_pattern(LED_RED);
|
||||
|
||||
uint8_t err[] = {CMD_ERR, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00};
|
||||
memcpy(err + 4, "FLASH FAILED", 12);
|
||||
ble_service_send_notify_flashing(err, sizeof(err));
|
||||
ESP_LOGI(TAG, "Transition: FLASHING -> ERROR_TARGET");
|
||||
} else if (event == EVENT_BLE_DISCONNECT) {
|
||||
ESP_LOGW(TAG, "BLE disconnect during FLASHING — flasher will finish, SM reset to IDLE");
|
||||
current_state = STATE_IDLE;
|
||||
led_set_pattern(LED_BLUE_BLINK);
|
||||
}
|
||||
break;
|
||||
|
||||
case STATE_SERIAL_BRIDGE:
|
||||
if (event == EVENT_BLE_INIT) {
|
||||
serial_bridge_stop();
|
||||
current_state = STATE_RECEIVING;
|
||||
led_set_pattern(LED_BLUE_BLINK);
|
||||
ESP_LOGI(TAG, "Transition: SERIAL_BRIDGE -> RECEIVING (re-deploy)");
|
||||
} else if (event == EVENT_BLE_DISCONNECT || event == EVENT_USB_DISCONNECT) {
|
||||
serial_bridge_stop();
|
||||
current_state = STATE_ERROR_TARGET;
|
||||
led_set_pattern(LED_RED);
|
||||
ESP_LOGI(TAG, "Transition: SERIAL_BRIDGE -> ERROR_TARGET (disconnect)");
|
||||
}
|
||||
break;
|
||||
|
||||
case STATE_ERROR_TARGET:
|
||||
case STATE_ERROR_CHECKSUM:
|
||||
if (event == EVENT_BLE_INIT) {
|
||||
ESP_LOGI(TAG, "Re-deploy from ERROR state — transition to RECEIVING");
|
||||
serial_bridge_stop();
|
||||
binary_parser_reset();
|
||||
current_state = STATE_RECEIVING;
|
||||
led_set_pattern(LED_BLUE_BLINK);
|
||||
} else if (event == EVENT_BUTTON_RETRY) {
|
||||
ESP_LOGI(TAG, "Button retry pressed, resetting to IDLE");
|
||||
binary_parser_reset();
|
||||
serial_bridge_stop();
|
||||
current_state = STATE_IDLE;
|
||||
led_set_pattern(LED_BLUE_BLINK);
|
||||
} else if (event == EVENT_BLE_DISCONNECT) {
|
||||
current_state = STATE_IDLE;
|
||||
led_set_pattern(LED_BLUE_BLINK);
|
||||
ESP_LOGI(TAG, "Transition: ERROR -> IDLE (BLE disconnect)");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
deployer_state_t state_machine_get_current(void)
|
||||
{
|
||||
return current_state;
|
||||
}
|
||||
|
||||
const char *state_machine_get_state_name(void)
|
||||
{
|
||||
switch (current_state) {
|
||||
case STATE_IDLE: return "IDLE";
|
||||
case STATE_RECEIVING: return "RECEIVING";
|
||||
case STATE_VERIFYING: return "VERIFYING";
|
||||
case STATE_FLASHING: return "FLASHING";
|
||||
case STATE_SERIAL_BRIDGE: return "SERIAL_BRIDGE";
|
||||
case STATE_ERROR_TARGET: return "ERROR_TARGET";
|
||||
case STATE_ERROR_CHECKSUM: return "ERROR_CHECKSUM";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
void state_machine_tick(void)
|
||||
{
|
||||
if (current_state == STATE_SERIAL_BRIDGE) {
|
||||
if (!usb_host_arduino_connected()) {
|
||||
state_machine_process_event(EVENT_USB_DISCONNECT, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
if ((current_state == STATE_ERROR_TARGET || current_state == STATE_ERROR_CHECKSUM)) {
|
||||
if (button_retry_pressed()) {
|
||||
state_machine_process_event(EVENT_BUTTON_RETRY, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue