fix: Phase B — readValue() state poll fallback for END ACK

Firmware:
- Add BLE_GATT_CHR_F_READ to flashing characteristic
- Read callback returns state_machine_get_current() as 1 byte
  (4=SERIAL_BRIDGE=success, 5/6=ERROR)

Frontend:
- Replace ineffective pendingAcks poll with readValue() poll
- CCCD refresh fire-and-forget (non-blocking)
- Race ackPromise (notify) vs readPoll (readValue)
- Clean up ackPromise resolver on readPoll win
- Cache v7 → v8
This commit is contained in:
a2nr 2026-06-30 17:35:23 +07:00
parent 9dda8fe3e6
commit 9918aafe41
3 changed files with 383 additions and 28 deletions

View File

@ -260,36 +260,90 @@ export class BLEHardwareDeployer {
console.log(`[BLE-CMD] ${cmdName} idx=${index}: write failed, still waiting for ACK`, e);
}
/* END (flash ~8s): Android BLE stack sometimes fails to deliver the
* notification through the normal characteristicvaluechanged handler
* while a writeValueWithResponse await is still in progress on some
* Qualcomm chipsets. Race the ackPromise against a poll loop that
* yields to the event loop every 200ms and checks pendingAcks (which
* the notification handler fills if the resolver path was missed). */
/* END (flash ~8s): Android BLE stack (Qualcomm) drops
* characteristicvaluechanged events after ~8s idle, so ACK
* notification is unreliable. Use readValue() to poll the
* firmware's state machine instead GATT Read is client-
* initiated and NOT affected by the notify idle-drop bug.
*
* CCCD refresh is fire-and-forget as a belt-and-suspenders:
* it didn't solve the issue on Qualcomm, but may help on
* other devices. */
if (cmd === CMD_END) {
const deadline = Date.now() + ackTimeout;
const pollPromise: Promise<void> = new Promise(resolve => {
(async () => {
while (Date.now() < deadline) {
if (this.pendingAcks.has(index)) {
this.pendingAcks.delete(index);
const entry = this.ackResolvers.get(index);
if (entry) {
clearTimeout(entry.timer);
this.ackResolvers.delete(index);
}
console.log(`[BLE-CMD] END idx=${index}: ACK received via poll (pendingAcks)`);
resolve();
return;
/* Fire-and-forget CCCD refresh (don't await — non-blocking). */
this.flashingChar!.stopNotifications().catch(() => {});
this.flashingChar!.startNotifications().then(
() => console.log('[BLE-CMD] END: CCCD re-subscribed (fire-and-forget)'),
(e) => console.log('[BLE-CMD] END: CCCD refresh failed (non-critical)', e)
);
/* Read-based state poll the RELIABLE path.
* State values from state_machine.h:
* 0=IDLE, 1=RECEIVING, 2=VERIFYING, 3=FLASHING (keep polling),
* 4=SERIAL_BRIDGE (success), 5=ERROR_TARGET, 6=ERROR_CHECKSUM */
const STATE_SERIAL_BRIDGE = 4;
const STATE_ERROR_TARGET = 5;
const STATE_ERROR_CHECKSUM = 6;
const readPoll = (async () => {
const deadline = Date.now() + ackTimeout;
let firstPoll = true;
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] END: readValue state=${state}`);
if (state === STATE_SERIAL_BRIDGE) {
console.log('[BLE-CMD] END: state=SERIAL_BRIDGE — flash success via read poll');
return 'ok' as const;
}
await new Promise<void>(r => setTimeout(r, 200));
if (state === STATE_ERROR_TARGET || state === STATE_ERROR_CHECKSUM) {
console.log(`[BLE-CMD] END: state=ERROR(${state}) — flash failed via read poll`);
throw new Error('Flash gagal di sisi firmware (state=' + state + ')');
}
/* state=FLASHING or VERIFYING — keep polling */
} catch (e) {
/* readValue failed (GATT error or timeout) keep
* retrying unless the connection itself is dead. */
if (!this.isConnected) {
throw new Error('Koneksi BLE terputus saat menunggu flash');
}
console.log('[BLE-CMD] END: readValue retry...', e);
}
console.log(`[BLE-CMD] END idx=${index}: poll deadline passed — ackPromise must win`);
resolve(); // resolve poll so the race falls through to ackPromise
})();
});
await Promise.race([ackPromise, pollPromise]);
console.log(`[BLE-CMD] END idx=${index}: ackPromise or poll resolved`);
/* First poll at 200ms (fast check if flash already done),
* then every 500ms. ~16 polls during 8s flash. */
await new Promise<void>(r => setTimeout(r, firstPoll ? 200 : 500));
firstPoll = false;
}
console.log('[BLE-CMD] END: readPoll deadline passed (30s)');
return 'timeout' as const;
})();
/* Race: ACK notify (fast path works on some devices) vs
* readValue poll (reliable path works everywhere). */
const ackWithLabel = ackPromise.then(() => 'ack' as const);
const result = await Promise.race([ackWithLabel, readPoll]);
console.log(`[BLE-CMD] END idx=${index}: resolved via ${result}`);
/* Clean up the ackPromise resolver if readPoll won
* prevents a dangling 30s timer. */
if (result !== 'ack') {
const entry = this.ackResolvers.get(index);
if (entry) {
clearTimeout(entry.timer);
this.ackResolvers.delete(index);
console.log(`[BLE-CMD] END: cleaned up ackPromise resolver (won via ${result})`);
}
}
if (result === 'timeout') {
throw new Error(`Timeout menunggu ACK untuk chunk ${index}`);
}
return;
}

View File

@ -1,5 +1,5 @@
// static/sw.js
const CACHE_VERSION = 'elemes-v6';
const CACHE_VERSION = 'elemes-v8';
const STATIC_CACHE = `${CACHE_VERSION}-static`;
const API_CACHE = `${CACHE_VERSION}-api`;
const ASSET_CACHE = `${CACHE_VERSION}-assets`;

View File

@ -0,0 +1,301 @@
#include <string.h>
#include "esp_log.h"
#include "esp_nimble_hci.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/ble_hs_id.h"
#include "host/ble_uuid.h"
#include "host/ble_gap.h"
#include "host/util/util.h"
#include "host/ble_sm.h"
#include "host/ble_att.h"
void ble_store_config_init(void);
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include "ble_service.h"
#include "serial_bridge.h"
#include "state_machine.h"
static const char *TAG = "BLE_SVC";
static ble_data_cb_t flashing_callback = NULL;
static ble_serial_cb_t serial_callback = NULL;
static uint16_t flashing_attr_handle;
static uint16_t serial_attr_handle;
static bool ble_connected = false;
static uint16_t conn_handle = 0;
static uint8_t own_addr_type = 0;
static const ble_uuid128_t service_uuid = BLE_UUID128_INIT(
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x4F, 0x49, 0x58, 0x4C, 0x45, 0x56
);
static const ble_uuid128_t flashing_char_uuid = BLE_UUID128_INIT(
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x4F, 0x49, 0x58, 0x4C, 0x45, 0x56
);
static const ble_uuid128_t serial_char_uuid = BLE_UUID128_INIT(
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x4F, 0x49, 0x58, 0x4C, 0x45, 0x56
);
static int ble_gap_event_cb(struct ble_gap_event *event, void *arg);
static int flashing_char_access_cb(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg)
{
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
/* Return current state machine state as 1 byte.
* Webapp polls this after END to detect flash completion:
* 0=IDLE, 1=RECEIVING, 2=VERIFYING, 3=FLASHING,
* 4=SERIAL_BRIDGE (success), 5=ERROR_TARGET, 6=ERROR_CHECKSUM */
uint8_t state = (uint8_t)state_machine_get_current();
os_mbuf_append(ctxt->om, &state, 1);
ESP_LOGD(TAG, "Flashing read: state=%d (%s)",
state, state_machine_get_state_name());
return 0;
}
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
size_t len = OS_MBUF_PKTLEN(ctxt->om);
uint8_t *data = malloc(len);
if (data) {
os_mbuf_copydata(ctxt->om, 0, len, data);
ESP_LOGI(TAG, "Flashing write: len=%d conn=%d handle=0x%04X cmd=0x%02X",
len, conn_handle, attr_handle, len > 0 ? data[0] : 0);
if (flashing_callback) {
flashing_callback(data, len);
}
free(data);
}
}
return 0;
}
static int serial_char_access_cb(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg)
{
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
size_t len = OS_MBUF_PKTLEN(ctxt->om);
uint8_t *data = malloc(len);
if (data) {
os_mbuf_copydata(ctxt->om, 0, len, data);
ESP_LOGI(TAG, "Serial write: len=%d conn=%d", len, conn_handle);
serial_bridge_on_ble_write(data, len);
free(data);
}
}
return 0;
}
static const struct ble_gatt_svc_def gatt_svcs[] = {
{
.type = BLE_GATT_SVC_TYPE_PRIMARY,
.uuid = &service_uuid.u,
.characteristics = (struct ble_gatt_chr_def[]) {
{
.uuid = &flashing_char_uuid.u,
.access_cb = flashing_char_access_cb,
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_NOTIFY,
.val_handle = &flashing_attr_handle
},
{
.uuid = &serial_char_uuid.u,
.access_cb = serial_char_access_cb,
.flags = BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY,
.val_handle = &serial_attr_handle
},
{
0
}
}
},
{
0
}
};
static void ble_restart_adv(void)
{
int rc;
struct ble_gap_adv_params adv_params;
uint8_t adv_data[31];
uint8_t adv_data_len = 0;
struct ble_hs_adv_fields fields;
memset(&fields, 0, sizeof(fields));
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
fields.name = (uint8_t *)"Velxio";
fields.name_len = 6;
fields.name_is_complete = 0;
fields.uuids16 = NULL;
fields.num_uuids16 = 0;
rc = ble_hs_adv_set_fields(&fields, adv_data, &adv_data_len, sizeof(adv_data));
if (rc != 0) {
ESP_LOGE(TAG, "ble_hs_adv_set_fields rc=%d", rc);
return;
}
rc = ble_gap_adv_set_data(adv_data, adv_data_len);
if (rc != 0) {
ESP_LOGE(TAG, "ble_gap_adv_set_data rc=%d", rc);
return;
}
memset(&adv_params, 0, sizeof(adv_params));
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER,
&adv_params, ble_gap_event_cb, NULL);
if (rc == 0) {
ESP_LOGI(TAG, "BLE advertising started");
} else {
ESP_LOGE(TAG, "ble_gap_adv_start rc=%d", rc);
}
}
static int ble_gap_event_cb(struct ble_gap_event *event, void *arg)
{
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
conn_handle = event->connect.conn_handle;
ble_connected = true;
ESP_LOGI(TAG, "BLE connected, handle=%d", conn_handle);
break;
case BLE_GAP_EVENT_DISCONNECT:
conn_handle = 0;
ble_connected = false;
ESP_LOGI(TAG, "BLE disconnected");
ble_restart_adv();
break;
case BLE_GAP_EVENT_ADV_COMPLETE:
ESP_LOGI(TAG, "ADV_COMPLETE, reason=%d", event->adv_complete.reason);
ble_restart_adv();
break;
case BLE_GAP_EVENT_SUBSCRIBE:
ESP_LOGI(TAG, "Subscribe attr_handle=%d notify=%d indicate=%d prev_notify=%d prev_indicate=%d",
event->subscribe.attr_handle,
event->subscribe.cur_notify,
event->subscribe.cur_indicate,
event->subscribe.prev_notify,
event->subscribe.prev_indicate);
break;
default:
break;
}
return 0;
}
static void ble_on_reset(int reason)
{
ESP_LOGE(TAG, "NimBLE host reset, reason=%d", reason);
}
static void ble_on_sync(void)
{
int rc;
rc = ble_hs_util_ensure_addr(1);
if (rc != 0) {
ESP_LOGE(TAG, "ble_hs_util_ensure_addr failed: %d", rc);
return;
}
rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
ESP_LOGE(TAG, "ble_hs_id_infer_auto failed: %d", rc);
return;
}
ESP_LOGI(TAG, "Address type: %d", own_addr_type);
rc = ble_att_set_preferred_mtu(255);
if (rc == 0) {
ESP_LOGI(TAG, "Preferred MTU set: 255");
} else {
ESP_LOGW(TAG, "ble_att_set_preferred_mtu failed: %d", rc);
}
ble_restart_adv();
}
static void host_task(void *param)
{
nimble_port_run();
}
void ble_service_init(void)
{
esp_nimble_hci_init();
nimble_port_init();
ble_hs_cfg.reset_cb = ble_on_reset;
ble_hs_cfg.sync_cb = ble_on_sync;
ble_hs_cfg.sm_io_cap = BLE_HS_IO_NO_INPUT_OUTPUT;
ble_hs_cfg.sm_sc = 0;
ble_svc_gap_init();
ble_svc_gatt_init();
ble_gatts_count_cfg(gatt_svcs);
ble_gatts_add_svcs(gatt_svcs);
ble_svc_gap_device_name_set("Velxio-Deployer");
ble_svc_gap_device_appearance_set(0x0080);
ble_store_config_init();
nimble_port_freertos_init(host_task);
ESP_LOGI(TAG, "BLE service initialized");
}
void ble_service_set_flashing_callback(ble_data_cb_t cb)
{
flashing_callback = cb;
}
void ble_service_set_serial_callback(ble_serial_cb_t cb)
{
serial_callback = cb;
}
void ble_service_send_notify_flashing(uint8_t *data, size_t len)
{
if (!ble_connected) return;
struct os_mbuf *om = ble_hs_mbuf_from_flat(data, len);
if (om) {
int rc = ble_gatts_notify_custom(conn_handle, flashing_attr_handle, om);
if (rc != 0) {
ESP_LOGW(TAG, "flashing notify failed: rc=%d handle=0x%04X conn=%d",
rc, flashing_attr_handle, conn_handle);
}
}
}
void ble_service_send_notify_serial(uint8_t *data, size_t len)
{
if (!ble_connected) return;
struct os_mbuf *om = ble_hs_mbuf_from_flat(data, len);
if (om) {
int rc = ble_gatts_notify_custom(conn_handle, serial_attr_handle, om);
if (rc != 0) {
ESP_LOGW(TAG, "serial notify failed: rc=%d handle=0x%04X conn=%d",
rc, serial_attr_handle, conn_handle);
}
}
}
bool ble_service_is_connected(void)
{
return ble_connected;
}