improved web ui, upload is sorta better, 5sec advertise btn
This commit is contained in:
+49
-29
@@ -23,14 +23,18 @@ export const CMD_PLAY = 0x04;
|
||||
export const CMD_STOP = 0x05;
|
||||
export const CMD_LIST = 0x06;
|
||||
export const CMD_DELETE = 0x07;
|
||||
export const CMD_SET_VOL = 0x08;
|
||||
export const CMD_GET_VOL = 0x09;
|
||||
|
||||
// Status-notification tags in byte 0 (see firmware):
|
||||
// 0x00 → upload progress: whole packet is uint32 BE byte count (files <16 MB)
|
||||
// 0x80|idx → list entry: [0x80|idx, bits, rate_kHz, duration_s]
|
||||
// 0xFF → list end: [0xFF, num_tracks, 0, 0]
|
||||
// 0xC0 → volume reply: [0xC0, percent, 0, 0]
|
||||
// 0xD0 → delete reply: [0xD0, idx, success, 0]
|
||||
// 0xFF → list end: [0xFF, num_tracks, 0, 0]
|
||||
const TAG_LIST_END = 0xff;
|
||||
const TAG_DELETE = 0xd0;
|
||||
const TAG_VOLUME = 0xc0;
|
||||
|
||||
export const MAX_TRACKS = 32;
|
||||
|
||||
@@ -159,6 +163,8 @@ export class BabyMobile extends EventTarget {
|
||||
|
||||
if (tag === TAG_LIST_END && dv.byteLength >= 2) {
|
||||
this._emit('list-end', { numTracks: dv.getUint8(1) });
|
||||
} else if (tag === TAG_VOLUME && dv.byteLength >= 2) {
|
||||
this._emit('volume', { percent: dv.getUint8(1) });
|
||||
} else if (tag === TAG_DELETE && dv.byteLength >= 3) {
|
||||
this._emit('deleted', { idx: dv.getUint8(1), success: dv.getUint8(2) === 1 });
|
||||
} else if ((tag & 0x80) && (tag & 0x7f) < MAX_TRACKS && dv.byteLength >= 4) {
|
||||
@@ -180,7 +186,7 @@ export class BabyMobile extends EventTarget {
|
||||
|
||||
async _writeCmd(bytes) {
|
||||
this._requireConnected();
|
||||
await this.cmd.writeValue(Uint8Array.from(bytes));
|
||||
await this.cmd.writeValueWithoutResponse(Uint8Array.from(bytes));
|
||||
}
|
||||
|
||||
async play(track) {
|
||||
@@ -193,6 +199,25 @@ export class BabyMobile extends EventTarget {
|
||||
this._log('⏹ Stop');
|
||||
}
|
||||
|
||||
// Set the persistent global volume (0–255 % of unity). The device echoes the
|
||||
// applied value via a 'volume' event.
|
||||
async setVolume(percent) {
|
||||
const p = Math.max(0, Math.min(255, Math.round(percent)));
|
||||
await this._writeCmd([CMD_SET_VOL, p]);
|
||||
}
|
||||
|
||||
// Ask the device for its current global volume; resolves to the percent.
|
||||
async getVolume(timeoutMs = 3000) {
|
||||
this._requireConnected();
|
||||
return new Promise((resolve, reject) => {
|
||||
const onVol = (e) => { cleanup(); resolve(e.detail.percent); };
|
||||
const cleanup = () => { clearTimeout(timer); this.removeEventListener('volume', onVol); };
|
||||
const timer = setTimeout(() => { cleanup(); reject(new Error('Volume read timed out')); }, timeoutMs);
|
||||
this.addEventListener('volume', onVol);
|
||||
this._writeCmd([CMD_GET_VOL]).catch((err) => { cleanup(); reject(err); });
|
||||
});
|
||||
}
|
||||
|
||||
// Returns [{ idx, bits, rate, durationSec }], sorted by slot.
|
||||
async listTracks(timeoutMs = 4000) {
|
||||
this._requireConnected();
|
||||
@@ -257,24 +282,17 @@ export class BabyMobile extends EventTarget {
|
||||
this._log(`Starting upload of ${total} bytes to slot ${track}…`);
|
||||
await this._writeCmd([CMD_UPLOAD_START, track & 0xff]);
|
||||
|
||||
// Send all data chunks in pipelined batches. Chrome on Windows serialises
|
||||
// individually-awaited writeValueWithoutResponse calls (one Promise per OS
|
||||
// event loop tick), which limits throughput to ~one packet per ~50 ms and
|
||||
// turns a 1.4 MB upload into a 6-minute ordeal. Firing PIPE writes before
|
||||
// awaiting them all fills multiple BLE connection events per await, saturating
|
||||
// the link. The firmware's 32 KB ring buffer absorbs bursts comfortably.
|
||||
const PIPE = 6; // packets in flight before each await (~1 KB per batch)
|
||||
for (let off = 0; off < total; off += DATA_CHUNK * PIPE) {
|
||||
const batch = [];
|
||||
for (let i = 0; i < PIPE && off + i * DATA_CHUNK < total; i++) {
|
||||
const s = off + i * DATA_CHUNK;
|
||||
batch.push(this.data.writeValueWithoutResponse(
|
||||
wavBytes.subarray(s, Math.min(s + DATA_CHUNK, total))));
|
||||
}
|
||||
await Promise.all(batch);
|
||||
// Send all data chunks sequentially. Chrome on Windows (WinRT BLE) only
|
||||
// allows one writeValueWithoutResponse in flight at a time; concurrent calls
|
||||
// fail with "GATT operation already in progress". Sequential writes are
|
||||
// reliable and throughput is governed by the BLE connection interval, not
|
||||
// by how many calls are in flight.
|
||||
for (let off = 0; off < total; off += DATA_CHUNK) {
|
||||
await this.data.writeValueWithoutResponse(
|
||||
wavBytes.subarray(off, Math.min(off + DATA_CHUNK, total)));
|
||||
// Cap the optimistic local count at 99%; the final 1% is filled only when
|
||||
// the device's finalize ack confirms the bytes actually landed in flash.
|
||||
report(Math.min(off + DATA_CHUNK * PIPE, Math.floor(total * 0.99)));
|
||||
report(Math.min(off + DATA_CHUNK, Math.floor(total * 0.99)));
|
||||
}
|
||||
|
||||
// Tell the firmware we're done. It will drain its ring buffer, write the
|
||||
@@ -302,20 +320,22 @@ export class BabyMobile extends EventTarget {
|
||||
return new Promise((resolve) => {
|
||||
if (this._statBytes >= target) return resolve();
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const tick = () => {
|
||||
if (this._statBytes >= target || Date.now() > deadline || !this.connected) {
|
||||
this.removeEventListener('progress', tick);
|
||||
resolve();
|
||||
}
|
||||
const cleanup = () => {
|
||||
clearInterval(poll);
|
||||
this.removeEventListener('progress', tick);
|
||||
this.removeEventListener('disconnected', onDisconnect);
|
||||
};
|
||||
const tick = () => {
|
||||
if (this._statBytes >= target || Date.now() > deadline) { cleanup(); resolve(); }
|
||||
};
|
||||
// Resolve on a real disconnect rather than polling .connected, which can
|
||||
// return false momentarily on some BLE stacks even when still connected.
|
||||
const onDisconnect = () => { cleanup(); resolve(); };
|
||||
this.addEventListener('progress', tick);
|
||||
// Fallback poll in case no further notifications arrive.
|
||||
this.addEventListener('disconnected', onDisconnect);
|
||||
// Fallback poll for timeout and when no further notifications arrive.
|
||||
const poll = setInterval(() => {
|
||||
if (this._statBytes >= target || Date.now() > deadline || !this.connected) {
|
||||
clearInterval(poll);
|
||||
this.removeEventListener('progress', tick);
|
||||
resolve();
|
||||
}
|
||||
if (this._statBytes >= target || Date.now() > deadline) { cleanup(); resolve(); }
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user