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".
|
local name just shows "Track N".
|
||||||
- **MTU:** data is chunked to 180 bytes to stay within a modest negotiated ATT
|
- **MTU:** data is chunked to 180 bytes to stay within a modest negotiated ATT
|
||||||
MTU (firmware char max is 240).
|
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;
|
export const MAX_TRACKS = 32;
|
||||||
|
|
||||||
// audioCmd.setMaxLen(240) / audioData.setMaxLen(240) on the firmware. We chunk the
|
// Keep below ATT MTU - 3. The firmware calls configPrphBandwidth(BANDWIDTH_MAX)
|
||||||
// data characteristic conservatively so it fits even a modest negotiated ATT MTU.
|
// so Chrome typically negotiates MTU 247 (244-byte payload). 180 stays well clear.
|
||||||
const DATA_CHUNK = 180;
|
const DATA_CHUNK = 180;
|
||||||
|
|
||||||
export function isSupported() {
|
export function isSupported() {
|
||||||
@@ -53,6 +53,7 @@ export class BabyMobile extends EventTarget {
|
|||||||
this._statBytes = 0;
|
this._statBytes = 0;
|
||||||
this._onStat = this._onStat.bind(this);
|
this._onStat = this._onStat.bind(this);
|
||||||
this._onDisconnect = this._onDisconnect.bind(this);
|
this._onDisconnect = this._onDisconnect.bind(this);
|
||||||
|
this._connecting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get connected() {
|
get connected() {
|
||||||
@@ -71,26 +72,57 @@ export class BabyMobile extends EventTarget {
|
|||||||
if (!isSupported()) {
|
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.');
|
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…');
|
if (this._connecting) throw new Error('Already connecting…');
|
||||||
this.device = await navigator.bluetooth.requestDevice({
|
this._connecting = true;
|
||||||
filters: [{ namePrefix: 'BabyMobile' }, { services: [SVC_UUID] }],
|
try {
|
||||||
optionalServices: [SVC_UUID],
|
// 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.device.addEventListener('gattserverdisconnected', this._onDisconnect);
|
this._teardown();
|
||||||
|
|
||||||
this._log(`Connecting to ${this.device.name || 'device'}…`);
|
this._log('Requesting device…');
|
||||||
this.server = await this.device.gatt.connect();
|
// Primary match is the 128-bit service UUID (rock solid). The name filter is
|
||||||
const svc = await this.server.getPrimaryService(SVC_UUID);
|
// a loose prefix because the advertised name may be radio-truncated.
|
||||||
this.cmd = await svc.getCharacteristic(CMD_UUID);
|
this.device = await navigator.bluetooth.requestDevice({
|
||||||
this.data = await svc.getCharacteristic(DATA_UUID);
|
filters: [{ services: [SVC_UUID] }, { namePrefix: 'Baby' }],
|
||||||
this.stat = await svc.getCharacteristic(STAT_UUID);
|
optionalServices: [SVC_UUID],
|
||||||
|
});
|
||||||
|
this.device.addEventListener('gattserverdisconnected', this._onDisconnect);
|
||||||
|
|
||||||
await this.stat.startNotifications();
|
await this._connectGatt(3);
|
||||||
this.stat.addEventListener('characteristicvaluechanged', this._onStat);
|
this._log('Connected.');
|
||||||
|
this._emit('connected', { name: this.device.name });
|
||||||
|
return this.device.name;
|
||||||
|
} finally {
|
||||||
|
this._connecting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._log('Connected.');
|
// Connect + discover with retries. A freshly-disconnected peripheral often
|
||||||
this._emit('connected', { name: this.device.name });
|
// leaves the link half-open for a moment; the first attempt then fails with
|
||||||
return this.device.name;
|
// "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() {
|
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() {
|
_onDisconnect() {
|
||||||
this._log('Disconnected.');
|
this._log('Disconnected.');
|
||||||
|
if (this.stat) {
|
||||||
|
try { this.stat.removeEventListener('characteristicvaluechanged', this._onStat); } catch {}
|
||||||
|
}
|
||||||
this.server = this.cmd = this.data = this.stat = null;
|
this.server = this.cmd = this.data = this.stat = null;
|
||||||
this._emit('disconnected', {});
|
this._emit('disconnected', {});
|
||||||
}
|
}
|
||||||
@@ -202,18 +249,22 @@ export class BabyMobile extends EventTarget {
|
|||||||
this._log(`Starting upload of ${total} bytes to slot ${track}…`);
|
this._log(`Starting upload of ${total} bytes to slot ${track}…`);
|
||||||
await this._writeCmd([CMD_UPLOAD_START, track & 0xff]);
|
await this._writeCmd([CMD_UPLOAD_START, track & 0xff]);
|
||||||
|
|
||||||
// writeWithoutResponse: await each chunk so the browser's queue provides
|
// Send all data chunks. Progress is tracked client-side (bytes sent).
|
||||||
// back-pressure. The firmware writes each packet to LittleFS synchronously.
|
// 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) {
|
for (let off = 0; off < total; off += DATA_CHUNK) {
|
||||||
const chunk = wavBytes.subarray(off, Math.min(off + DATA_CHUNK, total));
|
const chunk = wavBytes.subarray(off, Math.min(off + DATA_CHUNK, total));
|
||||||
await this.data.writeValueWithoutResponse(chunk);
|
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).
|
// Tell the firmware we're done. It will drain its ring buffer, write the
|
||||||
await this._waitForAck(total, 8000);
|
// track table, then send ONE finalize notification with the total byte count.
|
||||||
|
|
||||||
await this._writeCmd([CMD_UPLOAD_END]);
|
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;
|
const ok = acked >= total;
|
||||||
if (ok) {
|
if (ok) {
|
||||||
this._log(`✅ Upload complete: ${acked}/${total} bytes acknowledged.`);
|
this._log(`✅ Upload complete: ${acked}/${total} bytes acknowledged.`);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// sw.js — minimal offline cache so the PWA launches without a network.
|
// sw.js — minimal offline cache so the PWA launches without a network.
|
||||||
const CACHE = 'babymobile-v2';
|
const CACHE = 'babymobile-v3';
|
||||||
const ASSETS = [
|
const ASSETS = [
|
||||||
'./', './index.html', './styles.css',
|
'./', './index.html', './styles.css',
|
||||||
'./app.js', './ble.js', './wav.js',
|
'./app.js', './ble.js', './wav.js',
|
||||||
|
|||||||
@@ -185,12 +185,30 @@ BLECharacteristic audioStat = BLECharacteristic("12340004-0000-1000-8000-00805f9
|
|||||||
|
|
||||||
// BLE upload state
|
// BLE upload state
|
||||||
volatile bool g_bleUploading = false;
|
volatile bool g_bleUploading = false;
|
||||||
volatile uint32_t g_bleWriteAddr = 0;
|
volatile uint32_t g_bleWriteLen = 0;
|
||||||
volatile uint32_t g_bleWriteLen = 0;
|
|
||||||
|
|
||||||
#ifdef POC_INTERNAL_FLASH
|
#ifdef POC_INTERNAL_FLASH
|
||||||
File g_pocFile(InternalFS); // open file handle (read or write)
|
File g_pocFile(InternalFS); // open file handle (read or write)
|
||||||
uint8_t g_pocWriteTrack = 0; // track slot being written via BLE
|
uint8_t g_pocWriteTrack = 0; // track slot being written via BLE
|
||||||
|
#else
|
||||||
|
// Ring buffer between BLE callbacks and loop() flash writes.
|
||||||
|
// Callbacks return immediately; flash I/O (especially sector erase ~100ms)
|
||||||
|
// happens in loop() so the SoftDevice receive buffer never stalls.
|
||||||
|
// Size: IS25LP128F sector erase takes up to 300 ms; at ~56 KB/s upload rate
|
||||||
|
// that is ~17 KB of incoming data. 32 KB covers it with margin.
|
||||||
|
#define BLE_FLASH_BUF 32768u
|
||||||
|
static uint8_t g_bleBuf[BLE_FLASH_BUF];
|
||||||
|
static volatile uint32_t g_bleBufHead = 0; // advanced by BLE callback
|
||||||
|
static volatile uint32_t g_bleBufTail = 0; // advanced by loop()
|
||||||
|
static uint32_t g_bleFlashAddr = 0; // current flash write head
|
||||||
|
static uint32_t g_bleFlashStart = 0; // address where this upload began
|
||||||
|
static uint32_t g_bleFlashErased = 0; // end of last erased sector
|
||||||
|
static uint8_t g_bleWriteTrack = 0;
|
||||||
|
static volatile bool g_bleFinalizing = false;
|
||||||
|
|
||||||
|
// Shared next-free flash pointer — used by both BLE and serial upload paths.
|
||||||
|
// Reset to AUDIO_START_ADDR whenever track 0 is uploaded.
|
||||||
|
static uint32_t g_flashNextFree = AUDIO_START_ADDR;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -439,6 +457,17 @@ void loadTrackTable() {
|
|||||||
Serial.print("Loaded ");
|
Serial.print("Loaded ");
|
||||||
Serial.print(g_numTracks);
|
Serial.print(g_numTracks);
|
||||||
Serial.println(" tracks from flash");
|
Serial.println(" tracks from flash");
|
||||||
|
|
||||||
|
// Advance g_flashNextFree past all existing tracks so BLE/serial uploads
|
||||||
|
// to non-zero slots don't overwrite data from a previous session.
|
||||||
|
if (g_numTracks > 0) {
|
||||||
|
uint32_t hiWater = 0;
|
||||||
|
for (uint8_t i = 0; i < g_numTracks; i++) {
|
||||||
|
uint32_t trackEnd = g_trackStart[i] + g_trackLen[i];
|
||||||
|
if (trackEnd > hiWater) hiWater = trackEnd;
|
||||||
|
}
|
||||||
|
g_flashNextFree = ((hiWater + FLASH_SECTOR - 1) / FLASH_SECTOR) * FLASH_SECTOR;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,13 +622,21 @@ void audioCmd_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
|
|||||||
Serial.println(g_pocWriteTrack);
|
Serial.println(g_pocWriteTrack);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
if (len >= 4) {
|
if (len >= 2) {
|
||||||
g_bleWriteAddr = ((uint32_t)data[1] << 16) | ((uint32_t)data[2] << 8) | data[3];
|
g_bleWriteTrack = data[1];
|
||||||
g_bleUploading = true;
|
// Track 0 resets allocation; subsequent tracks append
|
||||||
// Erase 64K block at target
|
if (g_bleWriteTrack == 0) g_flashNextFree = AUDIO_START_ADDR;
|
||||||
flashEraseBlock64K(g_bleWriteAddr & ~(FLASH_BLOCK_64K - 1));
|
g_bleFlashAddr = g_flashNextFree;
|
||||||
Serial.print("BLE: Start write at 0x");
|
g_bleFlashStart = g_bleFlashAddr; // save start for finalize
|
||||||
Serial.println(g_bleWriteAddr, HEX);
|
g_bleFlashErased = g_bleFlashAddr; // nothing erased yet
|
||||||
|
g_bleBufHead = g_bleBufTail = 0;
|
||||||
|
g_bleFinalizing = false;
|
||||||
|
g_bleUploading = true;
|
||||||
|
g_bleWriteLen = 0;
|
||||||
|
Serial.print("BLE: Start write track ");
|
||||||
|
Serial.print(g_bleWriteTrack);
|
||||||
|
Serial.print(" at 0x");
|
||||||
|
Serial.println(g_bleFlashAddr, HEX);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
@@ -607,10 +644,17 @@ void audioCmd_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
|
|||||||
case 0x03: // Finish upload
|
case 0x03: // Finish upload
|
||||||
#ifdef POC_INTERNAL_FLASH
|
#ifdef POC_INTERNAL_FLASH
|
||||||
if (g_pocFile) g_pocFile.close();
|
if (g_pocFile) g_pocFile.close();
|
||||||
#endif
|
|
||||||
g_bleUploading = false;
|
g_bleUploading = false;
|
||||||
loadTrackTable();
|
loadTrackTable();
|
||||||
Serial.println("BLE: Upload complete");
|
Serial.println("BLE: Upload complete");
|
||||||
|
#else
|
||||||
|
// Signal loop() to finalize after ring buffer drains.
|
||||||
|
// Don't call loadTrackTable() here — we're in a BLE callback.
|
||||||
|
if (g_bleUploading) {
|
||||||
|
g_bleFinalizing = true;
|
||||||
|
Serial.println("BLE: Finalizing upload...");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 0x04: // Play track
|
case 0x04: // Play track
|
||||||
@@ -630,7 +674,6 @@ void audioCmd_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
|
|||||||
// End packet: [0xFF, num_tracks, 0, 0]
|
// End packet: [0xFF, num_tracks, 0, 0]
|
||||||
// Byte 0 >= 0x80 distinguishes list responses from upload-progress
|
// Byte 0 >= 0x80 distinguishes list responses from upload-progress
|
||||||
// packets (which always have byte 0 == 0x00 for files < 16 MB).
|
// packets (which always have byte 0 == 0x00 for files < 16 MB).
|
||||||
#ifdef POC_INTERNAL_FLASH
|
|
||||||
for (uint8_t i = 0; i < g_numTracks; i++) {
|
for (uint8_t i = 0; i < g_numTracks; i++) {
|
||||||
uint32_t audioBytes = g_trackLen[i] - g_trackDataOff[i];
|
uint32_t audioBytes = g_trackLen[i] - g_trackDataOff[i];
|
||||||
uint32_t bps = g_trackBits[i] / 8;
|
uint32_t bps = g_trackBits[i] / 8;
|
||||||
@@ -652,19 +695,35 @@ void audioCmd_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
|
|||||||
audioStat.notify(end, 4);
|
audioStat.notify(end, 4);
|
||||||
}
|
}
|
||||||
Serial.print("BLE: Listed "); Serial.print(g_numTracks); Serial.println(" tracks");
|
Serial.print("BLE: Listed "); Serial.print(g_numTracks); Serial.println(" tracks");
|
||||||
#endif
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 0x07: // Delete track
|
case 0x07: // Delete track
|
||||||
// data[1] = track index to delete.
|
// data[1] = track index to delete.
|
||||||
// Responds via audioStat: [0xD0, track_idx, success(0/1), 0]
|
// Responds via audioStat: [0xD0, track_idx, success(0/1), 0]
|
||||||
#ifdef POC_INTERNAL_FLASH
|
|
||||||
if (len >= 2) {
|
if (len >= 2) {
|
||||||
uint8_t trkNum = data[1];
|
uint8_t trkNum = data[1];
|
||||||
|
bool ok = false;
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
char fname[16];
|
char fname[16];
|
||||||
pocFilename(trkNum, fname);
|
pocFilename(trkNum, fname);
|
||||||
bool ok = InternalFS.remove(fname);
|
ok = InternalFS.remove(fname);
|
||||||
if (ok) loadTrackTable();
|
if (ok) loadTrackTable();
|
||||||
|
#else
|
||||||
|
if (trkNum < g_numTracks) {
|
||||||
|
// Shift entries down to close the gap
|
||||||
|
for (uint8_t j = trkNum; j < g_numTracks - 1; j++) {
|
||||||
|
g_trackStart[j] = g_trackStart[j+1];
|
||||||
|
g_trackLen[j] = g_trackLen[j+1];
|
||||||
|
g_trackBits[j] = g_trackBits[j+1];
|
||||||
|
g_trackRate[j] = g_trackRate[j+1];
|
||||||
|
g_trackDataOff[j] = g_trackDataOff[j+1];
|
||||||
|
}
|
||||||
|
g_numTracks--;
|
||||||
|
writeTrackTable();
|
||||||
|
loadTrackTable();
|
||||||
|
ok = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
uint8_t resp[4] = {0xD0, trkNum, (uint8_t)(ok ? 1 : 0), 0};
|
uint8_t resp[4] = {0xD0, trkNum, (uint8_t)(ok ? 1 : 0), 0};
|
||||||
audioStat.write(resp, 4);
|
audioStat.write(resp, 4);
|
||||||
audioStat.notify(resp, 4);
|
audioStat.notify(resp, 4);
|
||||||
@@ -672,7 +731,6 @@ void audioCmd_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
|
|||||||
Serial.print(trkNum);
|
Serial.print(trkNum);
|
||||||
Serial.println(ok ? " OK" : " FAILED");
|
Serial.println(ok ? " OK" : " FAILED");
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -687,39 +745,88 @@ void audioData_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
|
|||||||
g_pocFile.write(data, len);
|
g_pocFile.write(data, len);
|
||||||
g_bleWriteLen += len;
|
g_bleWriteLen += len;
|
||||||
#else
|
#else
|
||||||
// Erase new sectors as we cross boundaries
|
// Push into ring buffer; flash I/O happens in bleFlashTick() from loop()
|
||||||
uint32_t endAddr = g_bleWriteAddr + len;
|
// so this callback returns immediately without blocking the SoftDevice.
|
||||||
uint32_t currentSector = g_bleWriteAddr / FLASH_SECTOR;
|
for (uint16_t i = 0; i < len; i++) {
|
||||||
uint32_t endSector = (endAddr - 1) / FLASH_SECTOR;
|
uint32_t nextHead = (g_bleBufHead + 1) % BLE_FLASH_BUF;
|
||||||
for (uint32_t s = currentSector + 1; s <= endSector; s++) {
|
if (nextHead == g_bleBufTail) break; // full — drop tail (shouldn't happen at 16kHz)
|
||||||
flashEraseSector(s * FLASH_SECTOR);
|
g_bleBuf[g_bleBufHead] = data[i];
|
||||||
|
g_bleBufHead = nextHead;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write data page by page
|
|
||||||
uint16_t written = 0;
|
|
||||||
while (written < len) {
|
|
||||||
uint16_t pageOff = (g_bleWriteAddr + written) % FLASH_PAGE;
|
|
||||||
uint16_t chunk = min((uint16_t)(FLASH_PAGE - pageOff), (uint16_t)(len - written));
|
|
||||||
flashPageProgram(g_bleWriteAddr + written, data + written, chunk);
|
|
||||||
written += chunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_bleWriteAddr += len;
|
|
||||||
g_bleWriteLen += len;
|
g_bleWriteLen += len;
|
||||||
#endif
|
#endif
|
||||||
|
// Progress notifications are intentionally omitted here: sending one BLE
|
||||||
// Update status characteristic with bytes written
|
// notification per 180-byte packet floods the SoftDevice TX queue (~7 k
|
||||||
uint8_t stat[4];
|
// packets for a 1.3 MB file) and causes the last notifications to be
|
||||||
stat[0] = (g_bleWriteLen >> 24) & 0xFF;
|
// dropped. A single definitive notification is sent by bleFlashTick()
|
||||||
stat[1] = (g_bleWriteLen >> 16) & 0xFF;
|
// when finalization completes (after CMD_UPLOAD_END drains the ring buffer
|
||||||
stat[2] = (g_bleWriteLen >> 8) & 0xFF;
|
// and writes the track table).
|
||||||
stat[3] = g_bleWriteLen & 0xFF;
|
|
||||||
audioStat.write(stat, 4);
|
|
||||||
audioStat.notify(stat, 4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drain the BLE ring buffer to SPI flash one page at a time.
|
||||||
|
// Called from loop() to keep flash I/O off the BLE callback thread.
|
||||||
|
#ifndef POC_INTERNAL_FLASH
|
||||||
|
static void bleFlashTick() {
|
||||||
|
// Drain as much as we can without blocking too long
|
||||||
|
while (g_bleBufHead != g_bleBufTail) {
|
||||||
|
uint32_t avail = (g_bleBufHead - g_bleBufTail + BLE_FLASH_BUF) % BLE_FLASH_BUF;
|
||||||
|
uint16_t pageOff = (uint16_t)(g_bleFlashAddr % FLASH_PAGE);
|
||||||
|
uint16_t chunk = (uint16_t)min((uint32_t)(FLASH_PAGE - pageOff), avail);
|
||||||
|
if (chunk == 0) break;
|
||||||
|
|
||||||
|
// Lazy erase: erase the next sector just before we write into it
|
||||||
|
while (g_bleFlashAddr + chunk > g_bleFlashErased) {
|
||||||
|
flashEraseSector(g_bleFlashErased);
|
||||||
|
g_bleFlashErased += FLASH_SECTOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy chunk from ring buffer into a temp page buffer
|
||||||
|
uint8_t tmp[FLASH_PAGE];
|
||||||
|
for (uint16_t i = 0; i < chunk; i++) {
|
||||||
|
tmp[i] = g_bleBuf[(g_bleBufTail + i) % BLE_FLASH_BUF];
|
||||||
|
}
|
||||||
|
flashPageProgram(g_bleFlashAddr, tmp, chunk);
|
||||||
|
g_bleBufTail = (g_bleBufTail + chunk) % BLE_FLASH_BUF;
|
||||||
|
g_bleFlashAddr += chunk;
|
||||||
|
|
||||||
|
// Yield after each page so the rest of loop() stays responsive
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize when CMD 0x03 received and ring buffer is empty
|
||||||
|
if (g_bleFinalizing && g_bleBufHead == g_bleBufTail) {
|
||||||
|
uint32_t startAddr = g_bleFlashStart;
|
||||||
|
uint32_t bytesStored = g_bleFlashAddr - g_bleFlashStart;
|
||||||
|
g_trackStart[g_bleWriteTrack] = startAddr;
|
||||||
|
g_trackLen[g_bleWriteTrack] = bytesStored;
|
||||||
|
if (g_bleWriteTrack >= g_numTracks) g_numTracks = g_bleWriteTrack + 1;
|
||||||
|
|
||||||
|
// Advance shared free pointer to next sector boundary
|
||||||
|
g_flashNextFree = ((g_bleFlashAddr + FLASH_SECTOR - 1) / FLASH_SECTOR) * FLASH_SECTOR;
|
||||||
|
|
||||||
|
g_bleUploading = false;
|
||||||
|
g_bleFinalizing = false;
|
||||||
|
writeTrackTable();
|
||||||
|
loadTrackTable();
|
||||||
|
Serial.printf("BLE: Upload finalized — start=0x%08lX stored=%lu bytes\n", startAddr, bytesStored);
|
||||||
|
|
||||||
|
// Confirm to client: send final stat with bytes actually stored
|
||||||
|
uint8_t stat[4];
|
||||||
|
stat[0] = (bytesStored >> 24) & 0xFF;
|
||||||
|
stat[1] = (bytesStored >> 16) & 0xFF;
|
||||||
|
stat[2] = (bytesStored >> 8) & 0xFF;
|
||||||
|
stat[3] = bytesStored & 0xFF;
|
||||||
|
audioStat.write(stat, 4);
|
||||||
|
audioStat.notify(stat, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void setupBLE() {
|
void setupBLE() {
|
||||||
/* Note BLE requires a custom app to communicate, not regular bluetooth */
|
/* Note BLE requires a custom app to communicate, not regular bluetooth */
|
||||||
|
// Configure for maximum bandwidth so the SoftDevice allocates buffers large
|
||||||
|
// enough to accept 180-byte ATT payloads (MTU 183). Must be called before begin().
|
||||||
|
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
|
||||||
Bluefruit.begin();
|
Bluefruit.begin();
|
||||||
Bluefruit.setName("BabyMobile");
|
Bluefruit.setName("BabyMobile");
|
||||||
Bluefruit.setTxPower(0); // 0 dBm — save power, short range is fine
|
Bluefruit.setTxPower(0); // 0 dBm — save power, short range is fine
|
||||||
@@ -1187,10 +1294,9 @@ static uint32_t g_serBytesReceived = 0;
|
|||||||
static File g_serFile(InternalFS);
|
static File g_serFile(InternalFS);
|
||||||
#else
|
#else
|
||||||
// SPI flash upload: tracks are packed sequentially starting at AUDIO_START_ADDR.
|
// SPI flash upload: tracks are packed sequentially starting at AUDIO_START_ADDR.
|
||||||
// Uploading track 0 resets the allocation pointer.
|
// Uploading track 0 resets g_flashNextFree (shared with BLE upload path).
|
||||||
static uint32_t g_serFlashCurAddr = 0; // current write head
|
static uint32_t g_serFlashCurAddr = 0; // current write head
|
||||||
static uint32_t g_serFlashNextFree = AUDIO_START_ADDR;
|
static uint32_t g_serFlashErasedThru = 0; // highest erased byte address (exclusive)
|
||||||
static uint32_t g_serFlashErasedThru = 0; // highest erased byte address
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static void serUploadTick() {
|
static void serUploadTick() {
|
||||||
@@ -1221,8 +1327,8 @@ static void serUploadTick() {
|
|||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// Track 0 resets flash allocation
|
// Track 0 resets flash allocation
|
||||||
if (g_serTrack == 0) g_serFlashNextFree = AUDIO_START_ADDR;
|
if (g_serTrack == 0) g_flashNextFree = AUDIO_START_ADDR;
|
||||||
g_serFlashCurAddr = g_serFlashNextFree;
|
g_serFlashCurAddr = g_flashNextFree;
|
||||||
g_trackStart[g_serTrack] = g_serFlashCurAddr;
|
g_trackStart[g_serTrack] = g_serFlashCurAddr;
|
||||||
|
|
||||||
// Erase first sector now; subsequent sectors erased lazily during receive
|
// Erase first sector now; subsequent sectors erased lazily during receive
|
||||||
@@ -1376,8 +1482,8 @@ static void serUploadTick() {
|
|||||||
Serial.print("OK "); Serial.print(fsz);
|
Serial.print("OK "); Serial.print(fsz);
|
||||||
Serial.print("/"); Serial.println(g_serBytesReceived);
|
Serial.print("/"); Serial.println(g_serBytesReceived);
|
||||||
#else
|
#else
|
||||||
// Advance free pointer to next sector boundary
|
// Advance shared free pointer to next sector boundary
|
||||||
g_serFlashNextFree = ((g_serFlashCurAddr + FLASH_SECTOR - 1) / FLASH_SECTOR) * FLASH_SECTOR;
|
g_flashNextFree = ((g_serFlashCurAddr + FLASH_SECTOR - 1) / FLASH_SECTOR) * FLASH_SECTOR;
|
||||||
g_trackLen[g_serTrack] = g_serBytesReceived;
|
g_trackLen[g_serTrack] = g_serBytesReceived;
|
||||||
if (g_serTrack >= g_numTracks) g_numTracks = g_serTrack + 1;
|
if (g_serTrack >= g_numTracks) g_numTracks = g_serTrack + 1;
|
||||||
writeTrackTable();
|
writeTrackTable();
|
||||||
@@ -1397,6 +1503,11 @@ void loop() {
|
|||||||
// ---- Serial upload ----
|
// ---- Serial upload ----
|
||||||
serUploadTick();
|
serUploadTick();
|
||||||
|
|
||||||
|
#ifndef POC_INTERNAL_FLASH
|
||||||
|
// ---- BLE flash drain ----
|
||||||
|
bleFlashTick();
|
||||||
|
#endif
|
||||||
|
|
||||||
// ---- Refill audio buffers ----
|
// ---- Refill audio buffers ----
|
||||||
if (g_playing) {
|
if (g_playing) {
|
||||||
for (uint8_t b = 0; b < 2; b++) {
|
for (uint8_t b = 0; b < 2; b++) {
|
||||||
|
|||||||
Reference in New Issue
Block a user