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:
parent
9dda8fe3e6
commit
9918aafe41
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue