fix BLE upload: use g_bleFlashStart for correct track address and byte count on finalize
This commit is contained in:
@@ -75,3 +75,13 @@ and reuse `ble.js`'s protocol constants.
|
||||
local name just shows "Track N".
|
||||
- **MTU:** data is chunked to 180 bytes to stay within a modest negotiated ATT
|
||||
MTU (firmware char max is 240).
|
||||
- **Name shows as "BabyM" (truncated):** the 128-bit service UUID fills the 31-byte
|
||||
advertisement, leaving room for only 5 name characters. The browser can't read
|
||||
the full GAP name (that service is blocklisted in Web Bluetooth). Fix it in the
|
||||
firmware by moving the name to the scan response, which has its own 31 bytes:
|
||||
replace `Bluefruit.Advertising.addName();` with `Bluefruit.ScanResponse.addName();`.
|
||||
- **Reconnecting after a disconnect:** a just-disconnected peripheral leaves the
|
||||
link half-open briefly, so the first reconnect could fail until a page refresh.
|
||||
`connect()` now retries (3×, 600 ms backoff) after an explicit disconnect, which
|
||||
recovers without refreshing. Scanning matches on the service UUID first, so a
|
||||
truncated advertised name no longer breaks the name filter.
|
||||
|
||||
+76
-25
@@ -34,8 +34,8 @@ const TAG_DELETE = 0xd0;
|
||||
|
||||
export const MAX_TRACKS = 32;
|
||||
|
||||
// audioCmd.setMaxLen(240) / audioData.setMaxLen(240) on the firmware. We chunk the
|
||||
// data characteristic conservatively so it fits even a modest negotiated ATT MTU.
|
||||
// Keep below ATT MTU - 3. The firmware calls configPrphBandwidth(BANDWIDTH_MAX)
|
||||
// so Chrome typically negotiates MTU 247 (244-byte payload). 180 stays well clear.
|
||||
const DATA_CHUNK = 180;
|
||||
|
||||
export function isSupported() {
|
||||
@@ -53,6 +53,7 @@ export class BabyMobile extends EventTarget {
|
||||
this._statBytes = 0;
|
||||
this._onStat = this._onStat.bind(this);
|
||||
this._onDisconnect = this._onDisconnect.bind(this);
|
||||
this._connecting = false;
|
||||
}
|
||||
|
||||
get connected() {
|
||||
@@ -71,26 +72,57 @@ export class BabyMobile extends EventTarget {
|
||||
if (!isSupported()) {
|
||||
throw new Error('Web Bluetooth is not available in this browser. On iOS use the Bluefy browser; on desktop/Android use Chrome or Edge.');
|
||||
}
|
||||
this._log('Requesting device…');
|
||||
this.device = await navigator.bluetooth.requestDevice({
|
||||
filters: [{ namePrefix: 'BabyMobile' }, { services: [SVC_UUID] }],
|
||||
optionalServices: [SVC_UUID],
|
||||
});
|
||||
this.device.addEventListener('gattserverdisconnected', this._onDisconnect);
|
||||
if (this._connecting) throw new Error('Already connecting…');
|
||||
this._connecting = true;
|
||||
try {
|
||||
// Tear down any stale device/handle from a previous session so reconnects
|
||||
// don't trip over a half-open GATT or duplicate event listeners.
|
||||
this._teardown();
|
||||
|
||||
this._log(`Connecting to ${this.device.name || 'device'}…`);
|
||||
this.server = await this.device.gatt.connect();
|
||||
const svc = await this.server.getPrimaryService(SVC_UUID);
|
||||
this.cmd = await svc.getCharacteristic(CMD_UUID);
|
||||
this.data = await svc.getCharacteristic(DATA_UUID);
|
||||
this.stat = await svc.getCharacteristic(STAT_UUID);
|
||||
this._log('Requesting device…');
|
||||
// Primary match is the 128-bit service UUID (rock solid). The name filter is
|
||||
// a loose prefix because the advertised name may be radio-truncated.
|
||||
this.device = await navigator.bluetooth.requestDevice({
|
||||
filters: [{ services: [SVC_UUID] }, { namePrefix: 'Baby' }],
|
||||
optionalServices: [SVC_UUID],
|
||||
});
|
||||
this.device.addEventListener('gattserverdisconnected', this._onDisconnect);
|
||||
|
||||
await this.stat.startNotifications();
|
||||
this.stat.addEventListener('characteristicvaluechanged', this._onStat);
|
||||
await this._connectGatt(3);
|
||||
this._log('Connected.');
|
||||
this._emit('connected', { name: this.device.name });
|
||||
return this.device.name;
|
||||
} finally {
|
||||
this._connecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
this._log('Connected.');
|
||||
this._emit('connected', { name: this.device.name });
|
||||
return this.device.name;
|
||||
// Connect + discover with retries. A freshly-disconnected peripheral often
|
||||
// leaves the link half-open for a moment; the first attempt then fails with
|
||||
// "Connection failed"/"GATT operation failed" until a browser refresh. Retrying
|
||||
// after an explicit disconnect + short backoff recovers without a refresh.
|
||||
async _connectGatt(attempts) {
|
||||
let lastErr;
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
try {
|
||||
if (this.device.gatt.connected) this.device.gatt.disconnect();
|
||||
this._log(`Connecting${i ? ` (retry ${i})` : ''} to ${this.device.name || 'device'}…`);
|
||||
this.server = await this.device.gatt.connect();
|
||||
const svc = await this.server.getPrimaryService(SVC_UUID);
|
||||
this.cmd = await svc.getCharacteristic(CMD_UUID);
|
||||
this.data = await svc.getCharacteristic(DATA_UUID);
|
||||
this.stat = await svc.getCharacteristic(STAT_UUID);
|
||||
await this.stat.startNotifications();
|
||||
this.stat.addEventListener('characteristicvaluechanged', this._onStat);
|
||||
return;
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
this._log(`Connect attempt ${i + 1} failed: ${err.message || err}`);
|
||||
try { this.device.gatt.disconnect(); } catch {}
|
||||
if (i < attempts - 1) await new Promise((r) => setTimeout(r, 600));
|
||||
}
|
||||
}
|
||||
throw lastErr;
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
@@ -99,8 +131,23 @@ export class BabyMobile extends EventTarget {
|
||||
}
|
||||
}
|
||||
|
||||
// Fully detach the current device + listeners (used before a fresh connect).
|
||||
_teardown() {
|
||||
if (this.stat) {
|
||||
try { this.stat.removeEventListener('characteristicvaluechanged', this._onStat); } catch {}
|
||||
}
|
||||
if (this.device) {
|
||||
try { this.device.removeEventListener('gattserverdisconnected', this._onDisconnect); } catch {}
|
||||
try { if (this.device.gatt.connected) this.device.gatt.disconnect(); } catch {}
|
||||
}
|
||||
this.device = this.server = this.cmd = this.data = this.stat = null;
|
||||
}
|
||||
|
||||
_onDisconnect() {
|
||||
this._log('Disconnected.');
|
||||
if (this.stat) {
|
||||
try { this.stat.removeEventListener('characteristicvaluechanged', this._onStat); } catch {}
|
||||
}
|
||||
this.server = this.cmd = this.data = this.stat = null;
|
||||
this._emit('disconnected', {});
|
||||
}
|
||||
@@ -202,18 +249,22 @@ export class BabyMobile extends EventTarget {
|
||||
this._log(`Starting upload of ${total} bytes to slot ${track}…`);
|
||||
await this._writeCmd([CMD_UPLOAD_START, track & 0xff]);
|
||||
|
||||
// writeWithoutResponse: await each chunk so the browser's queue provides
|
||||
// back-pressure. The firmware writes each packet to LittleFS synchronously.
|
||||
// Send all data chunks. Progress is tracked client-side (bytes sent).
|
||||
// The firmware queues bytes into a ring buffer; actual flash writes happen
|
||||
// in the background. We do NOT poll per-packet notifications here — that
|
||||
// caused BLE notification-buffer overflow for large files.
|
||||
for (let off = 0; off < total; off += DATA_CHUNK) {
|
||||
const chunk = wavBytes.subarray(off, Math.min(off + DATA_CHUNK, total));
|
||||
await this.data.writeValueWithoutResponse(chunk);
|
||||
if (onProgress) onProgress(Math.min(off + chunk.length, total), total);
|
||||
if (onProgress) onProgress(off + chunk.length, total);
|
||||
}
|
||||
|
||||
// Wait until the device's acknowledged byte count catches up (or times out).
|
||||
await this._waitForAck(total, 8000);
|
||||
|
||||
// Tell the firmware we're done. It will drain its ring buffer, write the
|
||||
// track table, then send ONE finalize notification with the total byte count.
|
||||
await this._writeCmd([CMD_UPLOAD_END]);
|
||||
|
||||
// Wait for that single finalize notification (up to 20 s to allow flash writes).
|
||||
await this._waitForAck(total, 20000);
|
||||
const ok = acked >= total;
|
||||
if (ok) {
|
||||
this._log(`✅ Upload complete: ${acked}/${total} bytes acknowledged.`);
|
||||
|
||||
Reference in New Issue
Block a user