fixed most upload issues, updated schematic
This commit is contained in:
+112
-26
@@ -1,6 +1,6 @@
|
|||||||
// app.js — UI glue for the BabyMobile manager PWA.
|
// app.js — UI glue for the BabyMobile manager PWA.
|
||||||
import { BabyMobile, isSupported, MAX_TRACKS } from './ble.js';
|
import { BabyMobile, isSupported, MAX_TRACKS } from './ble.js';
|
||||||
import { fileToWav, RATES, BITS } from './wav.js';
|
import { decodeFileToMono, encodeWav, trimSamples, RATES, BITS } from './wav.js';
|
||||||
|
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
const dev = new BabyMobile();
|
const dev = new BabyMobile();
|
||||||
@@ -12,6 +12,8 @@ let slots = loadSlots();
|
|||||||
|
|
||||||
let pendingWav = null; // { wav, durationSec, rate, bits, name }
|
let pendingWav = null; // { wav, durationSec, rate, bits, name }
|
||||||
let deviceTracks = null; // last result of dev.listTracks(), or null
|
let deviceTracks = null; // last result of dev.listTracks(), or null
|
||||||
|
let currentFile = null; // the picked File
|
||||||
|
let decoded = null; // { samples, rate, srcRate } cached decode of currentFile
|
||||||
|
|
||||||
function loadSlots() {
|
function loadSlots() {
|
||||||
try { return JSON.parse(localStorage.getItem(SLOTS_KEY)) || {}; }
|
try { return JSON.parse(localStorage.getItem(SLOTS_KEY)) || {}; }
|
||||||
@@ -67,35 +69,93 @@ $('drop').addEventListener('drop', (e) => {
|
|||||||
});
|
});
|
||||||
fileInput.addEventListener('change', () => { if (fileInput.files.length) handleFile(fileInput.files[0]); });
|
fileInput.addEventListener('change', () => { if (fileInput.files.length) handleFile(fileInput.files[0]); });
|
||||||
|
|
||||||
|
// Decode the file (expensive) once per file/rate, then re-encode cheaply when
|
||||||
|
// the user tweaks bit-depth or trim.
|
||||||
async function handleFile(file) {
|
async function handleFile(file) {
|
||||||
const rate = parseInt($('rate').value, 10);
|
stopPreview();
|
||||||
const bits = parseInt($('bits').value, 10);
|
currentFile = file;
|
||||||
$('convert-info').textContent = `Converting "${file.name}"…`;
|
decoded = null;
|
||||||
pendingWav = null;
|
pendingWav = null;
|
||||||
$('btn-upload').disabled = true;
|
$('btn-upload').disabled = true;
|
||||||
|
$('btn-preview').disabled = true;
|
||||||
|
$('trim').hidden = true;
|
||||||
|
$('convert-info').textContent = `Decoding "${file.name}"…`;
|
||||||
try {
|
try {
|
||||||
const res = await fileToWav(file, { rate, bits });
|
const rate = parseInt($('rate').value, 10);
|
||||||
pendingWav = { ...res, name: file.name };
|
decoded = await decodeFileToMono(file, rate);
|
||||||
const sec = res.durationSec.toFixed(1);
|
$('trim-start').value = '0';
|
||||||
$('convert-info').innerHTML =
|
$('trim-end').value = '0';
|
||||||
`<b>${file.name}</b> → ${bits}-bit ${rate / 1000} kHz mono<br>` +
|
$('trim').hidden = false;
|
||||||
`${sec}s · ${fmtBytes(res.wav.byteLength)} (incl. 44-byte WAV header)`;
|
reEncode();
|
||||||
$('btn-upload').disabled = !dev.connected;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$('convert-info').textContent = 'Could not decode this file: ' + (err.message || err) +
|
$('convert-info').textContent = 'Could not decode this file: ' + (err.message || err) +
|
||||||
'. Try a WAV/MP3/M4A the browser can decode.';
|
'. Try a WAV/MP3/M4A the browser can decode.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-convert if the format options change while a file is staged.
|
function trimValues() {
|
||||||
['rate', 'bits'].forEach((id) => $(id).addEventListener('change', () => {
|
return {
|
||||||
if (fileInput.files.length) handleFile(fileInput.files[0]);
|
trimStart: Math.max(0, parseFloat($('trim-start').value) || 0),
|
||||||
}));
|
trimEnd: Math.max(0, parseFloat($('trim-end').value) || 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-encode the staged WAV from the cached decode (no re-decode).
|
||||||
|
function reEncode() {
|
||||||
|
if (!decoded || !currentFile) return;
|
||||||
|
const bits = parseInt($('bits').value, 10);
|
||||||
|
const { trimStart, trimEnd } = trimValues();
|
||||||
|
const fullDur = decoded.samples.length / decoded.rate;
|
||||||
|
const { wav, durationSec } = encodeWav(decoded.samples, decoded.rate, bits, { trimStart, trimEnd });
|
||||||
|
pendingWav = { wav, durationSec, rate: decoded.rate, bits, name: currentFile.name };
|
||||||
|
|
||||||
|
const trimmed = (trimStart || trimEnd) ? ` (trimmed from ${fullDur.toFixed(1)}s)` : '';
|
||||||
|
$('convert-info').innerHTML =
|
||||||
|
`<b>${currentFile.name}</b> → ${bits}-bit ${decoded.rate / 1000} kHz mono<br>` +
|
||||||
|
`${durationSec.toFixed(1)}s${trimmed} · ${fmtBytes(wav.byteLength)} (incl. 44-byte WAV header)`;
|
||||||
|
$('btn-upload').disabled = !dev.connected || durationSec <= 0;
|
||||||
|
$('btn-preview').disabled = durationSec <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing the rate needs a fresh decode; bits/trim only need a re-encode.
|
||||||
|
$('rate').addEventListener('change', () => { if (currentFile) handleFile(currentFile); });
|
||||||
|
['bits', 'trim-start', 'trim-end'].forEach((id) => $(id).addEventListener('input', () => { stopPreview(); reEncode(); }));
|
||||||
|
|
||||||
|
// ---- local preview of the trimmed clip ----
|
||||||
|
let previewCtx = null;
|
||||||
|
let previewSrc = null;
|
||||||
|
function stopPreview() {
|
||||||
|
if (previewSrc) { try { previewSrc.stop(); } catch {} previewSrc = null; }
|
||||||
|
if (previewCtx) { try { previewCtx.close(); } catch {} previewCtx = null; }
|
||||||
|
$('btn-preview').textContent = '▶ Preview';
|
||||||
|
}
|
||||||
|
$('btn-preview').addEventListener('click', () => {
|
||||||
|
if (previewSrc) { stopPreview(); return; }
|
||||||
|
if (!decoded) return;
|
||||||
|
const { trimStart, trimEnd } = trimValues();
|
||||||
|
const slice = trimSamples(decoded.samples, decoded.rate, trimStart, trimEnd);
|
||||||
|
if (!slice.length) return;
|
||||||
|
const AC = window.AudioContext || window.webkitAudioContext;
|
||||||
|
previewCtx = new AC();
|
||||||
|
const buf = previewCtx.createBuffer(1, slice.length, decoded.rate);
|
||||||
|
buf.getChannelData(0).set(slice);
|
||||||
|
previewSrc = previewCtx.createBufferSource();
|
||||||
|
previewSrc.buffer = buf;
|
||||||
|
previewSrc.connect(previewCtx.destination);
|
||||||
|
previewSrc.onended = stopPreview;
|
||||||
|
previewSrc.start();
|
||||||
|
$('btn-preview').textContent = '⏹ Stop';
|
||||||
|
});
|
||||||
|
|
||||||
// ---- upload ----
|
// ---- upload ----
|
||||||
$('btn-upload').addEventListener('click', async () => {
|
$('btn-upload').addEventListener('click', async () => {
|
||||||
if (!pendingWav) return;
|
if (!pendingWav) return;
|
||||||
const track = parseInt($('upload-slot').value, 10);
|
const track = parseInt($('upload-slot').value, 10);
|
||||||
|
if (occupiedSlots().has(track)) {
|
||||||
|
const name = slots[track]?.name;
|
||||||
|
if (!confirm(`Slot ${track} already holds ${name ? `"${name}"` : 'a track'}. Overwrite it?`)) return;
|
||||||
|
}
|
||||||
|
stopPreview();
|
||||||
$('btn-upload').disabled = true;
|
$('btn-upload').disabled = true;
|
||||||
const bar = $('upload-bar');
|
const bar = $('upload-bar');
|
||||||
bar.style.width = '0%';
|
bar.style.width = '0%';
|
||||||
@@ -120,10 +180,6 @@ $('btn-upload').addEventListener('click', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- playback ----
|
|
||||||
$('btn-play').addEventListener('click', () => dev.play(parseInt($('play-slot').value, 10)).catch((e) => log('' + e)));
|
|
||||||
$('btn-stop').addEventListener('click', () => dev.stop().catch((e) => log('' + e)));
|
|
||||||
|
|
||||||
// ---- track list ----
|
// ---- track list ----
|
||||||
$('btn-refresh').addEventListener('click', () => refresh());
|
$('btn-refresh').addEventListener('click', () => refresh());
|
||||||
|
|
||||||
@@ -138,8 +194,10 @@ async function refresh() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDelete(idx, onDevice) {
|
async function onDelete(idx, onDevice, name) {
|
||||||
|
const label = name ? `"${name}"` : `the track in slot ${idx}`;
|
||||||
if (onDevice) {
|
if (onDevice) {
|
||||||
|
if (!confirm(`Delete ${label} from the device? This can't be undone.`)) return;
|
||||||
try {
|
try {
|
||||||
const r = await dev.deleteTrack(idx);
|
const r = await dev.deleteTrack(idx);
|
||||||
if (r.success) {
|
if (r.success) {
|
||||||
@@ -153,6 +211,7 @@ async function onDelete(idx, onDevice) {
|
|||||||
log('Delete failed: ' + (err.message || err));
|
log('Delete failed: ' + (err.message || err));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (!confirm(`Forget ${label} from this browser's list? (It only clears the local record.)`)) return;
|
||||||
delete slots[idx]; saveSlots(); renderSlots();
|
delete slots[idx]; saveSlots(); renderSlots();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,6 +230,8 @@ function renderSlots() {
|
|||||||
durationSec: slots[k].durationSec, name: slots[k].name,
|
durationSec: slots[k].durationSec, name: slots[k].name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
updateUploadSlotOptions(); // keep the upload picker's "next free" in sync
|
||||||
|
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
list.innerHTML = `<li class="empty">${onDevice ? 'No tracks on the device yet.' : 'Connect to list device tracks.'}</li>`;
|
list.innerHTML = `<li class="empty">${onDevice ? 'No tracks on the device yet.' : 'Connect to list device tracks.'}</li>`;
|
||||||
return;
|
return;
|
||||||
@@ -185,11 +246,12 @@ function renderSlots() {
|
|||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'slot-actions';
|
actions.className = 'slot-actions';
|
||||||
const playBtn = btn('▶', 'Play', () => dev.play(t.idx).catch((e) => log('' + e)));
|
const playBtn = btn('▶', 'Play', () => dev.play(t.idx).catch((e) => log('' + e)));
|
||||||
playBtn.dataset.needsConn = '';
|
const stopBtn = btn('⏹', 'Stop', () => dev.stop().catch((e) => log('' + e)));
|
||||||
playBtn.disabled = !dev.connected;
|
playBtn.dataset.needsConn = ''; stopBtn.dataset.needsConn = '';
|
||||||
|
playBtn.disabled = stopBtn.disabled = !dev.connected;
|
||||||
const delBtn = btn('🗑', onDevice ? 'Delete from device' : 'Forget (local only)',
|
const delBtn = btn('🗑', onDevice ? 'Delete from device' : 'Forget (local only)',
|
||||||
() => onDelete(t.idx, onDevice));
|
() => onDelete(t.idx, onDevice, t.name));
|
||||||
actions.append(playBtn, delBtn);
|
actions.append(playBtn, stopBtn, delBtn);
|
||||||
li.append(actions);
|
li.append(actions);
|
||||||
list.append(li);
|
list.append(li);
|
||||||
}
|
}
|
||||||
@@ -213,9 +275,33 @@ function fillSelect(sel, values, fmt, selected) {
|
|||||||
}
|
}
|
||||||
fillSelect($('rate'), RATES, (v) => `${v / 1000} kHz`, 8000);
|
fillSelect($('rate'), RATES, (v) => `${v / 1000} kHz`, 8000);
|
||||||
fillSelect($('bits'), BITS, (v) => `${v}-bit`, 8);
|
fillSelect($('bits'), BITS, (v) => `${v}-bit`, 8);
|
||||||
for (const sel of [$('upload-slot'), $('play-slot')]) {
|
fillSelect($('play-slot'), Array.from({ length: MAX_TRACKS }, (_, i) => i), (v) => `Slot ${v}`, 0);
|
||||||
fillSelect(sel, Array.from({ length: MAX_TRACKS }, (_, i) => i), (v) => `Slot ${v}`, 0);
|
|
||||||
|
// Which slots currently hold a track: device truth when connected, else local memory.
|
||||||
|
function occupiedSlots() {
|
||||||
|
if (dev.connected && Array.isArray(deviceTracks)) return new Set(deviceTracks.map((t) => t.idx));
|
||||||
|
return new Set(Object.keys(slots).map(Number));
|
||||||
}
|
}
|
||||||
|
function nextFreeSlot() {
|
||||||
|
const occ = occupiedSlots();
|
||||||
|
for (let i = 0; i < MAX_TRACKS; i++) if (!occ.has(i)) return i;
|
||||||
|
return MAX_TRACKS - 1; // all full → default to the last slot
|
||||||
|
}
|
||||||
|
// Rebuild the upload-slot picker, label occupied slots, and default to next free.
|
||||||
|
function updateUploadSlotOptions() {
|
||||||
|
const occ = occupiedSlots();
|
||||||
|
const sel = $('upload-slot');
|
||||||
|
sel.innerHTML = '';
|
||||||
|
for (let i = 0; i < MAX_TRACKS; i++) {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = i;
|
||||||
|
const name = slots[i]?.name;
|
||||||
|
o.textContent = occ.has(i) ? `Slot ${i} — in use${name ? `: ${name}` : ''}` : `Slot ${i} — free`;
|
||||||
|
sel.append(o);
|
||||||
|
}
|
||||||
|
sel.value = String(nextFreeSlot());
|
||||||
|
}
|
||||||
|
updateUploadSlotOptions();
|
||||||
|
|
||||||
// ---- support banner ----
|
// ---- support banner ----
|
||||||
if (!isSupported()) {
|
if (!isSupported()) {
|
||||||
|
|||||||
+38
-18
@@ -34,9 +34,9 @@ const TAG_DELETE = 0xd0;
|
|||||||
|
|
||||||
export const MAX_TRACKS = 32;
|
export const MAX_TRACKS = 32;
|
||||||
|
|
||||||
// Keep below ATT MTU - 3. The firmware calls configPrphBandwidth(BANDWIDTH_MAX)
|
// ATT MTU is negotiated to 247 (firmware requests via requestMtuExchange(247)).
|
||||||
// so Chrome typically negotiates MTU 247 (244-byte payload). 180 stays well clear.
|
// Write-without-response payload = MTU - 3 = 244 bytes.
|
||||||
const DATA_CHUNK = 180;
|
const DATA_CHUNK = 244;
|
||||||
|
|
||||||
export function isSupported() {
|
export function isSupported() {
|
||||||
return typeof navigator !== 'undefined' && !!navigator.bluetooth;
|
return typeof navigator !== 'undefined' && !!navigator.bluetooth;
|
||||||
@@ -231,40 +231,60 @@ export class BabyMobile extends EventTarget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream a complete WAV file to a slot. `onProgress(sent, total)` is called as
|
// Stream a complete WAV file to a slot. `onProgress(shown, total)` reports a
|
||||||
// the firmware acknowledges bytes via the status characteristic.
|
// single monotonic value combining the locally-sent count and the device's
|
||||||
|
// acknowledged count.
|
||||||
async upload(track, wavBytes, onProgress) {
|
async upload(track, wavBytes, onProgress) {
|
||||||
this._requireConnected();
|
this._requireConnected();
|
||||||
const total = wavBytes.byteLength;
|
const total = wavBytes.byteLength;
|
||||||
this._statBytes = 0;
|
this._statBytes = 0;
|
||||||
let acked = 0;
|
let acked = 0;
|
||||||
|
|
||||||
const progressHandler = (ev) => {
|
// The optimistic local "sent" count races ahead of the device's ack
|
||||||
acked = ev.detail.bytes;
|
// notifications, and the two arrive interleaved (and acks can lag badly over
|
||||||
if (onProgress) onProgress(Math.min(acked, total), total);
|
// a slow mobile link). Reporting both directly makes the bar jump forward then
|
||||||
|
// snap backward. Clamp to a high-water mark so progress only ever advances.
|
||||||
|
let shown = 0;
|
||||||
|
const report = (n) => {
|
||||||
|
const v = Math.min(Math.max(0, n), total);
|
||||||
|
if (v > shown) { shown = v; if (onProgress) onProgress(shown, total); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const progressHandler = (ev) => { acked = ev.detail.bytes; report(acked); };
|
||||||
this.addEventListener('progress', progressHandler);
|
this.addEventListener('progress', progressHandler);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
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]);
|
||||||
|
|
||||||
// Send all data chunks. Progress is tracked client-side (bytes sent).
|
// Send all data chunks in pipelined batches. Chrome on Windows serialises
|
||||||
// The firmware queues bytes into a ring buffer; actual flash writes happen
|
// individually-awaited writeValueWithoutResponse calls (one Promise per OS
|
||||||
// in the background. We do NOT poll per-packet notifications here — that
|
// event loop tick), which limits throughput to ~one packet per ~50 ms and
|
||||||
// caused BLE notification-buffer overflow for large files.
|
// turns a 1.4 MB upload into a 6-minute ordeal. Firing PIPE writes before
|
||||||
for (let off = 0; off < total; off += DATA_CHUNK) {
|
// awaiting them all fills multiple BLE connection events per await, saturating
|
||||||
const chunk = wavBytes.subarray(off, Math.min(off + DATA_CHUNK, total));
|
// the link. The firmware's 32 KB ring buffer absorbs bursts comfortably.
|
||||||
await this.data.writeValueWithoutResponse(chunk);
|
const PIPE = 6; // packets in flight before each await (~1 KB per batch)
|
||||||
if (onProgress) onProgress(off + chunk.length, total);
|
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);
|
||||||
|
// 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)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell the firmware we're done. It will drain its ring buffer, write the
|
// 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.
|
// 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).
|
// Wait for that single finalize notification. Allow up to 60 s: at 300 ms/sector
|
||||||
await this._waitForAck(total, 20000);
|
// worst-case, a 1.4 MB file has ~350 sectors = 105 s of erase time. In practice
|
||||||
|
// most sectors erase in ~30 ms so finalization completes in well under 60 s.
|
||||||
|
await this._waitForAck(total, 60000);
|
||||||
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.`);
|
||||||
|
|||||||
+6
-11
@@ -13,8 +13,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<img src="./icon.svg" alt="" width="32" height="32">
|
<img src="./icon.svg" alt="BabyMobile" width="28" height="28">
|
||||||
<h1>BabyMobile</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="conn">
|
<div class="conn">
|
||||||
<span id="status-dot" class="dot"></span>
|
<span id="status-dot" class="dot"></span>
|
||||||
@@ -40,6 +39,11 @@
|
|||||||
<label>Format <select id="bits"></select></label>
|
<label>Format <select id="bits"></select></label>
|
||||||
<label>Rate <select id="rate"></select></label>
|
<label>Rate <select id="rate"></select></label>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="trim" class="opts trim" hidden>
|
||||||
|
<label>Trim start <input id="trim-start" type="number" min="0" step="0.1" value="0"><span class="unit">s</span></label>
|
||||||
|
<label>Trim end <input id="trim-end" type="number" min="0" step="0.1" value="0"><span class="unit">s</span></label>
|
||||||
|
<button id="btn-preview" class="mini" disabled>▶ Preview</button>
|
||||||
|
</div>
|
||||||
<p id="convert-info" class="info">No file selected.</p>
|
<p id="convert-info" class="info">No file selected.</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Upload to <select id="upload-slot" data-needs-conn disabled></select></label>
|
<label>Upload to <select id="upload-slot" data-needs-conn disabled></select></label>
|
||||||
@@ -48,15 +52,6 @@
|
|||||||
<div class="progress"><div id="upload-bar" class="bar"></div></div>
|
<div class="progress"><div id="upload-bar" class="bar"></div></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card">
|
|
||||||
<h2>2 · Play</h2>
|
|
||||||
<div class="row">
|
|
||||||
<label>Play <select id="play-slot" data-needs-conn disabled></select></label>
|
|
||||||
<button id="btn-play" data-needs-conn disabled>▶ Play</button>
|
|
||||||
<button id="btn-stop" data-needs-conn disabled>⏹ Stop</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<h2>Tracks on device</h2>
|
<h2>Tracks on device</h2>
|
||||||
|
|||||||
+14
-1
@@ -16,6 +16,7 @@ body {
|
|||||||
background: linear-gradient(160deg, var(--bg), var(--bg2));
|
background: linear-gradient(160deg, var(--bg), var(--bg2));
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
header {
|
header {
|
||||||
@@ -48,11 +49,14 @@ button:disabled { opacity: .4; cursor: not-allowed; }
|
|||||||
button.primary { background: linear-gradient(135deg, var(--accent2), var(--accent)); border: none; color: #1a0f3a; font-weight: 600; }
|
button.primary { background: linear-gradient(135deg, var(--accent2), var(--accent)); border: none; color: #1a0f3a; font-weight: 600; }
|
||||||
button.mini { padding: 6px 10px; border-radius: 8px; }
|
button.mini { padding: 6px 10px; border-radius: 8px; }
|
||||||
|
|
||||||
select {
|
select, input[type=number] {
|
||||||
font: inherit; background: #1c123e; color: var(--text);
|
font: inherit; background: #1c123e; color: var(--text);
|
||||||
border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px;
|
border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px;
|
||||||
}
|
}
|
||||||
|
input[type=number] { width: 4.5rem; }
|
||||||
label { display: inline-flex; align-items: center; gap: 8px; font-size: .9rem; color: var(--muted); }
|
label { display: inline-flex; align-items: center; gap: 8px; font-size: .9rem; color: var(--muted); }
|
||||||
|
.trim { align-items: center; }
|
||||||
|
.trim .unit { color: var(--muted); margin-left: -4px; }
|
||||||
|
|
||||||
.drop {
|
.drop {
|
||||||
border: 2px dashed var(--line); border-radius: 14px; padding: 26px 16px;
|
border: 2px dashed var(--line); border-radius: 14px; padding: 26px 16px;
|
||||||
@@ -87,3 +91,12 @@ label { display: inline-flex; align-items: center; gap: 8px; font-size: .9rem; c
|
|||||||
font-size: .78rem; color: var(--muted); max-height: 200px; overflow: auto; white-space: pre-wrap; margin: 0; }
|
font-size: .78rem; color: var(--muted); max-height: 200px; overflow: auto; white-space: pre-wrap; margin: 0; }
|
||||||
footer { text-align: center; color: var(--muted); font-size: .78rem; padding: 8px 16px calc(24px + env(safe-area-inset-bottom)); }
|
footer { text-align: center; color: var(--muted); font-size: .78rem; padding: 8px 16px calc(24px + env(safe-area-inset-bottom)); }
|
||||||
footer a { color: var(--accent); }
|
footer a { color: var(--accent); }
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
header { padding: 12px 12px calc(12px + env(safe-area-inset-top)); }
|
||||||
|
.conn { gap: 6px; font-size: .8rem; }
|
||||||
|
main { padding: 12px; gap: 12px; }
|
||||||
|
.card { padding: 14px; border-radius: 14px; }
|
||||||
|
.opts { gap: 12px; }
|
||||||
|
.slot-meta b { max-width: 42vw; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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-v3';
|
const CACHE = 'babymobile-v5';
|
||||||
const ASSETS = [
|
const ASSETS = [
|
||||||
'./', './index.html', './styles.css',
|
'./', './index.html', './styles.css',
|
||||||
'./app.js', './ble.js', './wav.js',
|
'./app.js', './ble.js', './wav.js',
|
||||||
|
|||||||
+20
-5
@@ -72,11 +72,26 @@ function buildWav(pcm, rate, bits) {
|
|||||||
return u8;
|
return u8;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns { wav: Uint8Array, durationSec, rate, bits, srcRate }.
|
// Decode a file to mono Float32 samples at `rate`. This is the expensive step,
|
||||||
export async function fileToWav(file, { rate = 8000, bits = 8 } = {}) {
|
// so callers cache the result and re-encode cheaply when bits/trim change.
|
||||||
|
// Returns { samples, rate, srcRate }.
|
||||||
|
export async function decodeFileToMono(file, rate = 8000) {
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const { samples, srcRate } = await decodeMono(arrayBuffer, rate);
|
const { samples, srcRate } = await decodeMono(arrayBuffer, rate);
|
||||||
const pcm = floatToPcm(samples, bits);
|
return { samples, rate, srcRate };
|
||||||
const wav = buildWav(pcm, rate, bits);
|
}
|
||||||
return { wav, durationSec: samples.length / rate, rate, bits, srcRate };
|
|
||||||
|
// Slice [trimStart, end-trimEnd] off the mono samples and return that range.
|
||||||
|
export function trimSamples(samples, rate, trimStart = 0, trimEnd = 0) {
|
||||||
|
const start = Math.max(0, Math.min(samples.length, Math.floor(trimStart * rate)));
|
||||||
|
const end = Math.max(start, samples.length - Math.max(0, Math.floor(trimEnd * rate)));
|
||||||
|
return samples.subarray(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode mono Float32 samples to a WAV. Returns { wav, durationSec }.
|
||||||
|
export function encodeWav(samples, rate, bits, { trimStart = 0, trimEnd = 0 } = {}) {
|
||||||
|
const sliced = trimSamples(samples, rate, trimStart, trimEnd);
|
||||||
|
const pcm = floatToPcm(sliced, bits);
|
||||||
|
const wav = buildWav(pcm, rate, bits);
|
||||||
|
return { wav, durationSec: sliced.length / rate };
|
||||||
}
|
}
|
||||||
|
|||||||
+716
-800
File diff suppressed because it is too large
Load Diff
+215
-83
@@ -2872,10 +2872,10 @@
|
|||||||
(uuid "017eac50-3406-4cc1-bd32-b83e5696234a")
|
(uuid "017eac50-3406-4cc1-bd32-b83e5696234a")
|
||||||
)
|
)
|
||||||
(junction
|
(junction
|
||||||
(at 311.16 90.17)
|
(at 71.12 153.67)
|
||||||
(diameter 0)
|
(diameter 0)
|
||||||
(color 0 0 0 0)
|
(color 0 0 0 0)
|
||||||
(uuid "19afce19-eefe-43da-9ff9-594d5e7450df")
|
(uuid "17e89eec-f620-4c7a-8887-8563a7dca664")
|
||||||
)
|
)
|
||||||
(junction
|
(junction
|
||||||
(at 330.2 145.97)
|
(at 330.2 145.97)
|
||||||
@@ -2896,10 +2896,28 @@
|
|||||||
(uuid "6d7a0476-c2d8-409f-acf0-c51d5cc44c54")
|
(uuid "6d7a0476-c2d8-409f-acf0-c51d5cc44c54")
|
||||||
)
|
)
|
||||||
(junction
|
(junction
|
||||||
(at 311.16 87.63)
|
(at 309.89 87.63)
|
||||||
(diameter 0)
|
(diameter 0)
|
||||||
(color 0 0 0 0)
|
(color 0 0 0 0)
|
||||||
(uuid "eb2c5735-191b-4d6e-b9b7-6f9a1ba9da7c")
|
(uuid "7f9aeab0-b298-49b0-95a9-13f903c592bd")
|
||||||
|
)
|
||||||
|
(junction
|
||||||
|
(at 311.16 90.17)
|
||||||
|
(diameter 0)
|
||||||
|
(color 0 0 0 0)
|
||||||
|
(uuid "b41da60f-ed92-4caa-94b4-a8f17f7b37f7")
|
||||||
|
)
|
||||||
|
(junction
|
||||||
|
(at 309.88 87.63)
|
||||||
|
(diameter 0)
|
||||||
|
(color 0 0 0 0)
|
||||||
|
(uuid "b4ef88db-519f-48de-b199-16343987ba1f")
|
||||||
|
)
|
||||||
|
(junction
|
||||||
|
(at 67.31 166.37)
|
||||||
|
(diameter 0)
|
||||||
|
(color 0 0 0 0)
|
||||||
|
(uuid "e2984591-bbbd-47a7-b9f3-f6e744a0177d")
|
||||||
)
|
)
|
||||||
(junction
|
(junction
|
||||||
(at 207.01 204.47)
|
(at 207.01 204.47)
|
||||||
@@ -2913,6 +2931,12 @@
|
|||||||
(color 0 0 0 0)
|
(color 0 0 0 0)
|
||||||
(uuid "ef3a8681-bfa6-40af-9816-ec3346f9cb23")
|
(uuid "ef3a8681-bfa6-40af-9816-ec3346f9cb23")
|
||||||
)
|
)
|
||||||
|
(junction
|
||||||
|
(at 304.8 87.63)
|
||||||
|
(diameter 0)
|
||||||
|
(color 0 0 0 0)
|
||||||
|
(uuid "fa1bd6ee-5560-42cc-8cc4-182475fa5265")
|
||||||
|
)
|
||||||
(no_connect
|
(no_connect
|
||||||
(at 223.52 234.95)
|
(at 223.52 234.95)
|
||||||
(uuid "519c324a-3d40-42a9-a963-f53dd2b596c0")
|
(uuid "519c324a-3d40-42a9-a963-f53dd2b596c0")
|
||||||
@@ -2953,6 +2977,16 @@
|
|||||||
)
|
)
|
||||||
(uuid "0a315227-f5b7-4de4-9dc2-5a9da0d1e499")
|
(uuid "0a315227-f5b7-4de4-9dc2-5a9da0d1e499")
|
||||||
)
|
)
|
||||||
|
(wire
|
||||||
|
(pts
|
||||||
|
(xy 304.8 91.44) (xy 304.72 91.44)
|
||||||
|
)
|
||||||
|
(stroke
|
||||||
|
(width 0)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(uuid "0e7e7f7d-d9d1-48e4-b0a4-efa3065d2db0")
|
||||||
|
)
|
||||||
(wire
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 342.9 92.69) (xy 342.9 92.61)
|
(xy 342.9 92.69) (xy 342.9 92.61)
|
||||||
@@ -3005,13 +3039,23 @@
|
|||||||
)
|
)
|
||||||
(wire
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 311.16 87.63) (xy 312.43 87.63)
|
(xy 67.31 166.37) (xy 67.31 171.45)
|
||||||
)
|
)
|
||||||
(stroke
|
(stroke
|
||||||
(width 0)
|
(width 0)
|
||||||
(type default)
|
(type default)
|
||||||
)
|
)
|
||||||
(uuid "24800943-9fa3-42fe-8fc7-e4e93239651c")
|
(uuid "1e67db82-9086-4cc6-acee-2e0c4028091c")
|
||||||
|
)
|
||||||
|
(wire
|
||||||
|
(pts
|
||||||
|
(xy 62.23 166.37) (xy 67.31 166.37)
|
||||||
|
)
|
||||||
|
(stroke
|
||||||
|
(width 0)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(uuid "1fbb2173-f466-4fb3-a1e1-1fd99f26203e")
|
||||||
)
|
)
|
||||||
(wire
|
(wire
|
||||||
(pts
|
(pts
|
||||||
@@ -3053,6 +3097,16 @@
|
|||||||
)
|
)
|
||||||
(uuid "33cecf5c-c05b-46be-b395-224afca830f4")
|
(uuid "33cecf5c-c05b-46be-b395-224afca830f4")
|
||||||
)
|
)
|
||||||
|
(wire
|
||||||
|
(pts
|
||||||
|
(xy 304.8 87.63) (xy 297.18 87.63)
|
||||||
|
)
|
||||||
|
(stroke
|
||||||
|
(width 0)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(uuid "34fe1a69-5ff4-4a92-871e-1af3f56feee9")
|
||||||
|
)
|
||||||
(wire
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 330.2 148.59) (xy 330.2 149.86)
|
(xy 330.2 148.59) (xy 330.2 149.86)
|
||||||
@@ -3233,6 +3287,16 @@
|
|||||||
)
|
)
|
||||||
(uuid "7a32a98d-a07b-4e23-97a7-ff5c2761b4a9")
|
(uuid "7a32a98d-a07b-4e23-97a7-ff5c2761b4a9")
|
||||||
)
|
)
|
||||||
|
(wire
|
||||||
|
(pts
|
||||||
|
(xy 67.31 171.45) (xy 65.96 171.45)
|
||||||
|
)
|
||||||
|
(stroke
|
||||||
|
(width 0)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(uuid "7a5c76e9-bfde-4599-a03c-144047a97246")
|
||||||
|
)
|
||||||
(wire
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 207.01 201.93) (xy 223.52 201.93)
|
(xy 207.01 201.93) (xy 223.52 201.93)
|
||||||
@@ -3243,6 +3307,16 @@
|
|||||||
)
|
)
|
||||||
(uuid "7b09f59c-2799-44c6-bf90-c378030ea9b1")
|
(uuid "7b09f59c-2799-44c6-bf90-c378030ea9b1")
|
||||||
)
|
)
|
||||||
|
(wire
|
||||||
|
(pts
|
||||||
|
(xy 62.23 153.67) (xy 71.12 153.67)
|
||||||
|
)
|
||||||
|
(stroke
|
||||||
|
(width 0)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(uuid "83ec5324-e898-46e6-9fe2-31d9a05b3da0")
|
||||||
|
)
|
||||||
(wire
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 334.01 90.15) (xy 334.01 90.17)
|
(xy 334.01 90.15) (xy 334.01 90.17)
|
||||||
@@ -3273,6 +3347,16 @@
|
|||||||
)
|
)
|
||||||
(uuid "8b2e0b76-8ce1-43a5-b900-90897c8f6ac4")
|
(uuid "8b2e0b76-8ce1-43a5-b900-90897c8f6ac4")
|
||||||
)
|
)
|
||||||
|
(wire
|
||||||
|
(pts
|
||||||
|
(xy 71.12 153.67) (xy 71.12 157.48)
|
||||||
|
)
|
||||||
|
(stroke
|
||||||
|
(width 0)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(uuid "8cbf3c35-fa13-44b4-bcf4-21252fe25fcb")
|
||||||
|
)
|
||||||
(wire
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 231.14 207.01) (xy 243.84 207.01)
|
(xy 231.14 207.01) (xy 243.84 207.01)
|
||||||
@@ -3283,6 +3367,26 @@
|
|||||||
)
|
)
|
||||||
(uuid "8f5e7827-d21e-468e-b028-9b178eb97914")
|
(uuid "8f5e7827-d21e-468e-b028-9b178eb97914")
|
||||||
)
|
)
|
||||||
|
(wire
|
||||||
|
(pts
|
||||||
|
(xy 304.8 87.63) (xy 304.8 91.44)
|
||||||
|
)
|
||||||
|
(stroke
|
||||||
|
(width 0)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(uuid "8fbf48d3-9d5a-45d9-8e37-a4596be807cd")
|
||||||
|
)
|
||||||
|
(wire
|
||||||
|
(pts
|
||||||
|
(xy 309.89 87.63) (xy 309.88 87.63)
|
||||||
|
)
|
||||||
|
(stroke
|
||||||
|
(width 0)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(uuid "930181c0-0f67-43b3-8e66-2256b00ebbc0")
|
||||||
|
)
|
||||||
(wire
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 302.26 162.56) (xy 299.72 162.56)
|
(xy 302.26 162.56) (xy 299.72 162.56)
|
||||||
@@ -3303,6 +3407,16 @@
|
|||||||
)
|
)
|
||||||
(uuid "968c6461-fd30-4fe2-9334-56f61262ca7c")
|
(uuid "968c6461-fd30-4fe2-9334-56f61262ca7c")
|
||||||
)
|
)
|
||||||
|
(wire
|
||||||
|
(pts
|
||||||
|
(xy 67.31 166.37) (xy 78.74 166.37)
|
||||||
|
)
|
||||||
|
(stroke
|
||||||
|
(width 0)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(uuid "9a505b6a-5abe-47a4-9e70-9865514e51a2")
|
||||||
|
)
|
||||||
(wire
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 312.43 90.17) (xy 311.16 90.17)
|
(xy 312.43 90.17) (xy 311.16 90.17)
|
||||||
@@ -3325,7 +3439,7 @@
|
|||||||
)
|
)
|
||||||
(wire
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 311.16 90.17) (xy 309.89 90.17)
|
(xy 309.89 90.17) (xy 311.16 90.17)
|
||||||
)
|
)
|
||||||
(stroke
|
(stroke
|
||||||
(width 0)
|
(width 0)
|
||||||
@@ -3383,6 +3497,16 @@
|
|||||||
)
|
)
|
||||||
(uuid "b103c0d7-a6ac-40b0-9282-0e3442120b6c")
|
(uuid "b103c0d7-a6ac-40b0-9282-0e3442120b6c")
|
||||||
)
|
)
|
||||||
|
(wire
|
||||||
|
(pts
|
||||||
|
(xy 309.88 87.63) (xy 304.8 87.63)
|
||||||
|
)
|
||||||
|
(stroke
|
||||||
|
(width 0)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(uuid "b4e0a463-68c6-49f4-96e9-2451c3680296")
|
||||||
|
)
|
||||||
(wire
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 223.52 217.17) (xy 207.01 217.17)
|
(xy 223.52 217.17) (xy 207.01 217.17)
|
||||||
@@ -3473,6 +3597,26 @@
|
|||||||
)
|
)
|
||||||
(uuid "cf357f80-93fc-47ae-8a9d-c3f2a82281d2")
|
(uuid "cf357f80-93fc-47ae-8a9d-c3f2a82281d2")
|
||||||
)
|
)
|
||||||
|
(wire
|
||||||
|
(pts
|
||||||
|
(xy 309.88 85.09) (xy 311.16 85.09)
|
||||||
|
)
|
||||||
|
(stroke
|
||||||
|
(width 0)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(uuid "d23b020f-474a-44c7-aa50-5d2d6a637b91")
|
||||||
|
)
|
||||||
|
(wire
|
||||||
|
(pts
|
||||||
|
(xy 71.12 157.48) (xy 71.04 157.48)
|
||||||
|
)
|
||||||
|
(stroke
|
||||||
|
(width 0)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(uuid "d5374591-14af-4a6a-a74f-9e6b20508561")
|
||||||
|
)
|
||||||
(wire
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 231.14 237.49) (xy 243.84 237.49)
|
(xy 231.14 237.49) (xy 243.84 237.49)
|
||||||
@@ -3523,6 +3667,16 @@
|
|||||||
)
|
)
|
||||||
(uuid "dc56447b-2913-4073-a3e6-ae9a14e1130a")
|
(uuid "dc56447b-2913-4073-a3e6-ae9a14e1130a")
|
||||||
)
|
)
|
||||||
|
(wire
|
||||||
|
(pts
|
||||||
|
(xy 71.12 153.67) (xy 78.74 153.67)
|
||||||
|
)
|
||||||
|
(stroke
|
||||||
|
(width 0)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(uuid "dce908f3-84d4-4a63-89a0-3e8809dbb8a5")
|
||||||
|
)
|
||||||
(wire
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 334.01 90.17) (xy 334.02 90.17)
|
(xy 334.01 90.17) (xy 334.02 90.17)
|
||||||
@@ -3563,6 +3717,16 @@
|
|||||||
)
|
)
|
||||||
(uuid "f96fe0e4-f1ba-4802-b049-1ac2454eeab6")
|
(uuid "f96fe0e4-f1ba-4802-b049-1ac2454eeab6")
|
||||||
)
|
)
|
||||||
|
(wire
|
||||||
|
(pts
|
||||||
|
(xy 309.88 87.63) (xy 309.88 85.09)
|
||||||
|
)
|
||||||
|
(stroke
|
||||||
|
(width 0)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(uuid "f9b0891a-84da-4d7e-9086-65a34b6b7d6f")
|
||||||
|
)
|
||||||
(wire
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 339.09 76.18) (xy 339.17 76.18)
|
(xy 339.09 76.18) (xy 339.17 76.18)
|
||||||
@@ -3654,7 +3818,7 @@
|
|||||||
(uuid "21f44609-2138-4f22-9db6-f9e7fee8972d")
|
(uuid "21f44609-2138-4f22-9db6-f9e7fee8972d")
|
||||||
)
|
)
|
||||||
(label "C_3V3"
|
(label "C_3V3"
|
||||||
(at 159.58 167.43 270)
|
(at 55.96 171.45 180)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -3693,7 +3857,7 @@
|
|||||||
)
|
)
|
||||||
(uuid "39dfef0c-d7a2-4482-bca9-5affd0a9afcc")
|
(uuid "39dfef0c-d7a2-4482-bca9-5affd0a9afcc")
|
||||||
)
|
)
|
||||||
(label "AUDIO_PWM"
|
(label "LED1"
|
||||||
(at 85.09 95.25 180)
|
(at 85.09 95.25 180)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
@@ -3813,7 +3977,7 @@
|
|||||||
)
|
)
|
||||||
(uuid "7e061299-bff5-40d1-8f87-e3efcbd5b5b0")
|
(uuid "7e061299-bff5-40d1-8f87-e3efcbd5b5b0")
|
||||||
)
|
)
|
||||||
(label "LED1"
|
(label "C_BTN1"
|
||||||
(at 85.09 80.01 180)
|
(at 85.09 80.01 180)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
@@ -3824,7 +3988,7 @@
|
|||||||
(uuid "82233f70-9329-4389-8977-125c2201a8e6")
|
(uuid "82233f70-9329-4389-8977-125c2201a8e6")
|
||||||
)
|
)
|
||||||
(label "FLASH_CS"
|
(label "FLASH_CS"
|
||||||
(at 78.74 166.37 180)
|
(at 62.23 166.37 180)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -3844,7 +4008,7 @@
|
|||||||
(uuid "848da8f0-b353-4b3c-9a06-b30dcd05db07")
|
(uuid "848da8f0-b353-4b3c-9a06-b30dcd05db07")
|
||||||
)
|
)
|
||||||
(label "GND"
|
(label "GND"
|
||||||
(at 151.14 167.58 270)
|
(at 61.04 157.48 180)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -3873,15 +4037,15 @@
|
|||||||
)
|
)
|
||||||
(uuid "94725878-e74f-4b21-aae5-a56d4b5f5068")
|
(uuid "94725878-e74f-4b21-aae5-a56d4b5f5068")
|
||||||
)
|
)
|
||||||
(label "GND"
|
(label "AMP_SD"
|
||||||
(at 322.59 95.25 270)
|
(at 85.09 99.06 180)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
)
|
)
|
||||||
(justify right bottom)
|
(justify right bottom)
|
||||||
)
|
)
|
||||||
(uuid "9ba0110e-0516-473b-b196-7a8965125707")
|
(uuid "968fcbdf-0690-43cd-87a6-f65d215b6d57")
|
||||||
)
|
)
|
||||||
(label "VO+"
|
(label "VO+"
|
||||||
(at 340.36 160.02 0)
|
(at 340.36 160.02 0)
|
||||||
@@ -3924,7 +4088,7 @@
|
|||||||
(uuid "b0235f9d-665c-4f41-be2f-d227623085bc")
|
(uuid "b0235f9d-665c-4f41-be2f-d227623085bc")
|
||||||
)
|
)
|
||||||
(label "GND"
|
(label "GND"
|
||||||
(at 295.91 95.15 270)
|
(at 294.72 91.44 180)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -3964,7 +4128,7 @@
|
|||||||
(uuid "ba54329f-b24b-491c-af5b-8195107bd63a")
|
(uuid "ba54329f-b24b-491c-af5b-8195107bd63a")
|
||||||
)
|
)
|
||||||
(label "C_VBAT"
|
(label "C_VBAT"
|
||||||
(at 312.43 87.63 180)
|
(at 297.18 87.63 180)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -3983,7 +4147,7 @@
|
|||||||
)
|
)
|
||||||
(uuid "bf9261b2-0bea-41a0-aa2a-4209adebed25")
|
(uuid "bf9261b2-0bea-41a0-aa2a-4209adebed25")
|
||||||
)
|
)
|
||||||
(label "C_BTN1"
|
(label "AUDIO_PWM"
|
||||||
(at 85.09 76.2 180)
|
(at 85.09 76.2 180)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
@@ -4023,16 +4187,6 @@
|
|||||||
)
|
)
|
||||||
(uuid "debd7b52-0059-4c0b-bf7c-967d3c2772e8")
|
(uuid "debd7b52-0059-4c0b-bf7c-967d3c2772e8")
|
||||||
)
|
)
|
||||||
(label "FLASH_CS"
|
|
||||||
(at 159.58 157.43 90)
|
|
||||||
(effects
|
|
||||||
(font
|
|
||||||
(size 1.27 1.27)
|
|
||||||
)
|
|
||||||
(justify left bottom)
|
|
||||||
)
|
|
||||||
(uuid "e2af49fb-22e1-4190-a130-4ab73a10b921")
|
|
||||||
)
|
|
||||||
(label "GND"
|
(label "GND"
|
||||||
(at 308.53 157.48 180)
|
(at 308.53 157.48 180)
|
||||||
(effects
|
(effects
|
||||||
@@ -4044,17 +4198,7 @@
|
|||||||
(uuid "e7b3cd1a-4907-40c3-92a2-ed993d774d88")
|
(uuid "e7b3cd1a-4907-40c3-92a2-ed993d774d88")
|
||||||
)
|
)
|
||||||
(label "C_3V3"
|
(label "C_3V3"
|
||||||
(at 151.14 157.58 90)
|
(at 62.23 153.67 180)
|
||||||
(effects
|
|
||||||
(font
|
|
||||||
(size 1.27 1.27)
|
|
||||||
)
|
|
||||||
(justify left bottom)
|
|
||||||
)
|
|
||||||
(uuid "efd4637c-fa54-44ce-86ca-590813c3a03a")
|
|
||||||
)
|
|
||||||
(label "C_3V3"
|
|
||||||
(at 78.74 153.67 180)
|
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -4063,26 +4207,6 @@
|
|||||||
)
|
)
|
||||||
(uuid "f1103770-6762-4d62-b659-6b7212eb3807")
|
(uuid "f1103770-6762-4d62-b659-6b7212eb3807")
|
||||||
)
|
)
|
||||||
(label "C_VBAT"
|
|
||||||
(at 311.16 85.09 180)
|
|
||||||
(effects
|
|
||||||
(font
|
|
||||||
(size 1.27 1.27)
|
|
||||||
)
|
|
||||||
(justify right bottom)
|
|
||||||
)
|
|
||||||
(uuid "f1a32f8a-5ce2-4e1f-9c31-201dda2521a1")
|
|
||||||
)
|
|
||||||
(label "C_VBAT"
|
|
||||||
(at 295.91 85.15 90)
|
|
||||||
(effects
|
|
||||||
(font
|
|
||||||
(size 1.27 1.27)
|
|
||||||
)
|
|
||||||
(justify left bottom)
|
|
||||||
)
|
|
||||||
(uuid "f4e3eb90-54e4-46d9-ab89-62e9cba84a0b")
|
|
||||||
)
|
|
||||||
(label "SPI_SCK"
|
(label "SPI_SCK"
|
||||||
(at 125.73 95.25 0)
|
(at 125.73 95.25 0)
|
||||||
(effects
|
(effects
|
||||||
@@ -4093,7 +4217,7 @@
|
|||||||
)
|
)
|
||||||
(uuid "fa336a55-f4b8-4536-98b8-03cd9dc53b95")
|
(uuid "fa336a55-f4b8-4536-98b8-03cd9dc53b95")
|
||||||
)
|
)
|
||||||
(label "V_BAT"
|
(label "AMP_SD"
|
||||||
(at 307.34 148.59 180)
|
(at 307.34 148.59 180)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
@@ -4789,7 +4913,7 @@
|
|||||||
)
|
)
|
||||||
(symbol
|
(symbol
|
||||||
(lib_id "bm:C")
|
(lib_id "bm:C")
|
||||||
(at 151.14 162.58 0)
|
(at 66.04 157.48 270)
|
||||||
(unit 1)
|
(unit 1)
|
||||||
(exclude_from_sim no)
|
(exclude_from_sim no)
|
||||||
(in_bom yes)
|
(in_bom yes)
|
||||||
@@ -4797,7 +4921,7 @@
|
|||||||
(dnp no)
|
(dnp no)
|
||||||
(uuid "4ea7caff-53f9-4c00-bf01-2c0cfc1ebd9c")
|
(uuid "4ea7caff-53f9-4c00-bf01-2c0cfc1ebd9c")
|
||||||
(property "Reference" "C3"
|
(property "Reference" "C3"
|
||||||
(at 154.14 159.58 0)
|
(at 69.04 160.48 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -4805,7 +4929,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Value" "100nF"
|
(property "Value" "100nF"
|
||||||
(at 154.14 165.58 0)
|
(at 63.04 160.48 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -4813,7 +4937,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Footprint" "Capacitor_SMD:C_0805_2012Metric"
|
(property "Footprint" "Capacitor_SMD:C_0805_2012Metric"
|
||||||
(at 151.14 162.58 0)
|
(at 66.04 157.48 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -4822,7 +4946,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Datasheet" ""
|
(property "Datasheet" ""
|
||||||
(at 151.14 162.58 0)
|
(at 66.04 157.48 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -4831,7 +4955,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Description" ""
|
(property "Description" ""
|
||||||
(at 151.14 162.58 0)
|
(at 66.04 157.48 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -4923,7 +5047,7 @@
|
|||||||
)
|
)
|
||||||
(symbol
|
(symbol
|
||||||
(lib_id "bm:R")
|
(lib_id "bm:R")
|
||||||
(at 159.58 162.43 0)
|
(at 60.96 171.45 270)
|
||||||
(unit 1)
|
(unit 1)
|
||||||
(exclude_from_sim no)
|
(exclude_from_sim no)
|
||||||
(in_bom yes)
|
(in_bom yes)
|
||||||
@@ -4931,7 +5055,7 @@
|
|||||||
(dnp no)
|
(dnp no)
|
||||||
(uuid "696b8ca8-97f4-4851-97cc-70c56c8f54f5")
|
(uuid "696b8ca8-97f4-4851-97cc-70c56c8f54f5")
|
||||||
(property "Reference" "R2"
|
(property "Reference" "R2"
|
||||||
(at 162.58 159.43 0)
|
(at 63.96 174.45 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -4939,7 +5063,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Value" "10K"
|
(property "Value" "10K"
|
||||||
(at 162.58 165.43 0)
|
(at 57.96 174.45 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -4947,7 +5071,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Footprint" "Resistor_SMD:R_0805_2012Metric"
|
(property "Footprint" "Resistor_SMD:R_0805_2012Metric"
|
||||||
(at 159.58 162.43 0)
|
(at 60.96 171.45 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -4956,7 +5080,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Datasheet" ""
|
(property "Datasheet" ""
|
||||||
(at 159.58 162.43 0)
|
(at 60.96 171.45 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -4965,7 +5089,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Description" ""
|
(property "Description" ""
|
||||||
(at 159.58 162.43 0)
|
(at 60.96 171.45 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -5555,7 +5679,6 @@
|
|||||||
(in_bom yes)
|
(in_bom yes)
|
||||||
(on_board yes)
|
(on_board yes)
|
||||||
(dnp no)
|
(dnp no)
|
||||||
(fields_autoplaced yes)
|
|
||||||
(uuid "90218aa1-4311-468c-8ce8-89d467c4ac29")
|
(uuid "90218aa1-4311-468c-8ce8-89d467c4ac29")
|
||||||
(property "Reference" "R4"
|
(property "Reference" "R4"
|
||||||
(at 311.15 142.24 90)
|
(at 311.15 142.24 90)
|
||||||
@@ -5600,6 +5723,15 @@
|
|||||||
(hide yes)
|
(hide yes)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
(property "Todo" "Is this necessary?"
|
||||||
|
(at 310.896 140.462 90)
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1.27 1.27)
|
||||||
|
(italic yes)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
(pin "1"
|
(pin "1"
|
||||||
(uuid "0b135b1d-91e6-4233-b45c-8d0fafdbdcfb")
|
(uuid "0b135b1d-91e6-4233-b45c-8d0fafdbdcfb")
|
||||||
)
|
)
|
||||||
@@ -5788,7 +5920,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Value" "0.33uF"
|
(property "Value" "0.33uF"
|
||||||
(at 345.9 90.61 0)
|
(at 347.726 90.17 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -5814,7 +5946,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Description" "Ceramic"
|
(property "Description" "Ceramic"
|
||||||
(at 342.9 87.61 0)
|
(at 348.234 87.63 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -5922,7 +6054,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Value" "10uF"
|
(property "Value" "10uF"
|
||||||
(at 347.17 73.18 0)
|
(at 347.218 71.882 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -5948,7 +6080,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Description" "Ceramic"
|
(property "Description" "Ceramic"
|
||||||
(at 344.17 76.18 0)
|
(at 344.424 71.12 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -6309,7 +6441,7 @@
|
|||||||
)
|
)
|
||||||
(symbol
|
(symbol
|
||||||
(lib_id "bm:C")
|
(lib_id "bm:C")
|
||||||
(at 295.91 90.15 0)
|
(at 299.72 91.44 270)
|
||||||
(unit 1)
|
(unit 1)
|
||||||
(exclude_from_sim no)
|
(exclude_from_sim no)
|
||||||
(in_bom yes)
|
(in_bom yes)
|
||||||
@@ -6317,7 +6449,7 @@
|
|||||||
(dnp no)
|
(dnp no)
|
||||||
(uuid "d225c3fa-9586-478d-a854-6d14dbaa4bfd")
|
(uuid "d225c3fa-9586-478d-a854-6d14dbaa4bfd")
|
||||||
(property "Reference" "C1"
|
(property "Reference" "C1"
|
||||||
(at 298.91 87.15 0)
|
(at 302.72 94.44 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -6325,7 +6457,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Value" "10uF"
|
(property "Value" "10uF"
|
||||||
(at 298.91 93.15 0)
|
(at 296.72 94.44 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -6333,7 +6465,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Footprint" "Capacitor_SMD:C_0805_2012Metric"
|
(property "Footprint" "Capacitor_SMD:C_0805_2012Metric"
|
||||||
(at 295.91 90.15 0)
|
(at 299.72 91.44 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -6342,7 +6474,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Datasheet" ""
|
(property "Datasheet" ""
|
||||||
(at 295.91 90.15 0)
|
(at 299.72 91.44 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -6351,7 +6483,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Description" "Ceramic"
|
(property "Description" "Ceramic"
|
||||||
(at 295.91 90.15 0)
|
(at 299.466 97.028 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
|
|||||||
@@ -183,7 +183,8 @@ BLECharacteristic audioCmd = BLECharacteristic("12340002-0000-1000-8000-00805f9
|
|||||||
BLECharacteristic audioData = BLECharacteristic("12340003-0000-1000-8000-00805f9b34fb");
|
BLECharacteristic audioData = BLECharacteristic("12340003-0000-1000-8000-00805f9b34fb");
|
||||||
BLECharacteristic audioStat = BLECharacteristic("12340004-0000-1000-8000-00805f9b34fb");
|
BLECharacteristic audioStat = BLECharacteristic("12340004-0000-1000-8000-00805f9b34fb");
|
||||||
|
|
||||||
// BLE upload state
|
// BLE connection / upload state
|
||||||
|
volatile bool g_bleConnected = false; // true while a GATT connection is active
|
||||||
volatile bool g_bleUploading = false;
|
volatile bool g_bleUploading = false;
|
||||||
volatile uint32_t g_bleWriteLen = 0;
|
volatile uint32_t g_bleWriteLen = 0;
|
||||||
|
|
||||||
@@ -192,17 +193,20 @@ 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
|
#else
|
||||||
// Ring buffer between BLE callbacks and loop() flash writes.
|
// Ring buffer between BLE callbacks and loop() flash writes.
|
||||||
// Callbacks return immediately; flash I/O (especially sector erase ~100ms)
|
// Callbacks return immediately; flash I/O happens in loop() so the SoftDevice
|
||||||
// happens in loop() so the SoftDevice receive buffer never stalls.
|
// receive buffer never stalls. Sector erase is async (non-blocking): bleFlashTick()
|
||||||
// Size: IS25LP128F sector erase takes up to 300 ms; at ~56 KB/s upload rate
|
// kicks off the erase and returns; loop() polls completion each iteration. This
|
||||||
// that is ~17 KB of incoming data. 32 KB covers it with margin.
|
// prevents the ring buffer from filling during the ~30–300 ms erase window.
|
||||||
#define BLE_FLASH_BUF 32768u
|
// Buffer: 64 KB handles worst-case 300 ms erase at up to ~200 KB/s BLE throughput.
|
||||||
|
#define BLE_FLASH_BUF 65536u
|
||||||
static uint8_t g_bleBuf[BLE_FLASH_BUF];
|
static uint8_t g_bleBuf[BLE_FLASH_BUF];
|
||||||
static volatile uint32_t g_bleBufHead = 0; // advanced by BLE callback
|
static volatile uint32_t g_bleBufHead = 0; // advanced by BLE callback
|
||||||
static volatile uint32_t g_bleBufTail = 0; // advanced by loop()
|
static volatile uint32_t g_bleBufTail = 0; // advanced by loop()
|
||||||
static uint32_t g_bleFlashAddr = 0; // current flash write head
|
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_bleFlashStart = 0; // address where this upload began
|
||||||
static uint32_t g_bleFlashErased = 0; // end of last erased sector
|
static uint32_t g_bleFlashErased = 0; // upper boundary of erased flash
|
||||||
|
static bool g_bleErasing = false; // async sector erase in progress
|
||||||
|
static uint32_t g_bleNotifyThresh = 0; // next addr to send progress notify
|
||||||
static uint8_t g_bleWriteTrack = 0;
|
static uint8_t g_bleWriteTrack = 0;
|
||||||
static volatile bool g_bleFinalizing = false;
|
static volatile bool g_bleFinalizing = false;
|
||||||
|
|
||||||
@@ -306,6 +310,17 @@ void flashWaitBusy() {
|
|||||||
SPI.endTransaction();
|
SPI.endTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-blocking busy check: reads the WIP bit without spinning.
|
||||||
|
bool flashIsBusy() {
|
||||||
|
SPI.beginTransaction(flashSPI);
|
||||||
|
flashSelect();
|
||||||
|
SPI.transfer(CMD_READ_SR1);
|
||||||
|
uint8_t sr = SPI.transfer(0);
|
||||||
|
flashDeselect();
|
||||||
|
SPI.endTransaction();
|
||||||
|
return (sr & 0x01) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
void flashWriteEnable() {
|
void flashWriteEnable() {
|
||||||
SPI.beginTransaction(flashSPI);
|
SPI.beginTransaction(flashSPI);
|
||||||
flashSelect();
|
flashSelect();
|
||||||
@@ -564,12 +579,26 @@ bool msc_start_stop_cb(uint8_t power_condition, bool start, bool load_eject) {
|
|||||||
|
|
||||||
void ble_connect_cb(uint16_t conn_handle) {
|
void ble_connect_cb(uint16_t conn_handle) {
|
||||||
Serial.println("BLE connected");
|
Serial.println("BLE connected");
|
||||||
|
g_bleConnected = true;
|
||||||
g_lastActivity = millis();
|
g_lastActivity = millis();
|
||||||
|
|
||||||
|
// Request fast connection parameters and maximum throughput features.
|
||||||
|
// The central may accept, renegotiate, or ignore these — all safe.
|
||||||
|
BLEConnection* conn = Bluefruit.Connection(conn_handle);
|
||||||
|
if (conn) {
|
||||||
|
conn->requestConnectionParameter(6); // 6×1.25ms = 7.5ms interval
|
||||||
|
conn->requestMtuExchange(247); // 244-byte ATT payload
|
||||||
|
conn->requestDataLengthUpdate(); // LE Data Length Extension
|
||||||
|
conn->requestPHY(BLE_GAP_PHY_2MBPS); // 2M PHY if supported
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ble_disconnect_cb(uint16_t conn_handle, uint8_t reason) {
|
void ble_disconnect_cb(uint16_t conn_handle, uint8_t reason) {
|
||||||
Serial.println("BLE disconnected");
|
Serial.println("BLE disconnected");
|
||||||
|
g_bleConnected = false;
|
||||||
g_bleUploading = false;
|
g_bleUploading = false;
|
||||||
|
g_bleFinalizing = false;
|
||||||
|
g_bleErasing = false; // stop tracking the in-progress erase; it will finish in HW
|
||||||
}
|
}
|
||||||
|
|
||||||
// BLE command characteristic: receives commands
|
// BLE command characteristic: receives commands
|
||||||
@@ -627,8 +656,10 @@ void audioCmd_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
|
|||||||
// Track 0 resets allocation; subsequent tracks append
|
// Track 0 resets allocation; subsequent tracks append
|
||||||
if (g_bleWriteTrack == 0) g_flashNextFree = AUDIO_START_ADDR;
|
if (g_bleWriteTrack == 0) g_flashNextFree = AUDIO_START_ADDR;
|
||||||
g_bleFlashAddr = g_flashNextFree;
|
g_bleFlashAddr = g_flashNextFree;
|
||||||
g_bleFlashStart = g_bleFlashAddr; // save start for finalize
|
g_bleFlashStart = g_bleFlashAddr;
|
||||||
g_bleFlashErased = g_bleFlashAddr; // nothing erased yet
|
g_bleFlashErased = g_bleFlashAddr; // nothing erased yet
|
||||||
|
g_bleErasing = false;
|
||||||
|
g_bleNotifyThresh = g_bleFlashAddr;
|
||||||
g_bleBufHead = g_bleBufTail = 0;
|
g_bleBufHead = g_bleBufTail = 0;
|
||||||
g_bleFinalizing = false;
|
g_bleFinalizing = false;
|
||||||
g_bleUploading = true;
|
g_bleUploading = true;
|
||||||
@@ -763,37 +794,92 @@ void audioData_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
|
|||||||
// and writes the track table).
|
// and writes the track table).
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drain the BLE ring buffer to SPI flash one page at a time.
|
// Drain the BLE ring buffer to SPI flash.
|
||||||
// Called from loop() to keep flash I/O off the BLE callback thread.
|
// Sector erases are asynchronous: we issue the erase command and return immediately
|
||||||
|
// so loop() keeps running (and the ring buffer keeps draining from BLE callbacks).
|
||||||
|
// On the next call we poll the WIP bit to confirm completion before writing.
|
||||||
|
// This prevents the 30–300 ms erase window from filling the ring buffer.
|
||||||
|
// A pre-erase is also kicked off as soon as we start writing each sector so
|
||||||
|
// the next sector is ready before we reach it. Writes stop while any erase is
|
||||||
|
// in progress because the flash ignores page-program when WIP=1.
|
||||||
#ifndef POC_INTERNAL_FLASH
|
#ifndef POC_INTERNAL_FLASH
|
||||||
static void bleFlashTick() {
|
static void bleFlashTick() {
|
||||||
// Drain as much as we can without blocking too long
|
if (!g_bleUploading && !g_bleFinalizing) return;
|
||||||
|
|
||||||
|
// Poll async sector erase completion.
|
||||||
|
if (g_bleErasing && !flashIsBusy()) {
|
||||||
|
g_bleErasing = false;
|
||||||
|
g_bleFlashErased += FLASH_SECTOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain all available ring-buffer data into flash, one page per iteration.
|
||||||
while (g_bleBufHead != g_bleBufTail) {
|
while (g_bleBufHead != g_bleBufTail) {
|
||||||
|
if (g_bleErasing) break; // never write while an erase is in progress
|
||||||
uint32_t avail = (g_bleBufHead - g_bleBufTail + BLE_FLASH_BUF) % BLE_FLASH_BUF;
|
uint32_t avail = (g_bleBufHead - g_bleBufTail + BLE_FLASH_BUF) % BLE_FLASH_BUF;
|
||||||
uint16_t pageOff = (uint16_t)(g_bleFlashAddr % FLASH_PAGE);
|
uint16_t pageOff = (uint16_t)(g_bleFlashAddr % FLASH_PAGE);
|
||||||
uint16_t chunk = (uint16_t)min((uint32_t)(FLASH_PAGE - pageOff), avail);
|
uint16_t chunk = (uint16_t)min((uint32_t)(FLASH_PAGE - pageOff), avail);
|
||||||
if (chunk == 0) break;
|
if (chunk == 0) break;
|
||||||
|
|
||||||
// Lazy erase: erase the next sector just before we write into it
|
if (g_bleFlashAddr + chunk > g_bleFlashErased) {
|
||||||
while (g_bleFlashAddr + chunk > g_bleFlashErased) {
|
// Write pointer has reached the erased boundary — need another sector erased.
|
||||||
flashEraseSector(g_bleFlashErased);
|
if (!g_bleErasing) {
|
||||||
g_bleFlashErased += FLASH_SECTOR;
|
flashWriteEnable();
|
||||||
|
SPI.beginTransaction(flashSPI);
|
||||||
|
flashSelect();
|
||||||
|
SPI.transfer(CMD_SECT_ERASE);
|
||||||
|
SPI.transfer((g_bleFlashErased >> 16) & 0xFF);
|
||||||
|
SPI.transfer((g_bleFlashErased >> 8) & 0xFF);
|
||||||
|
SPI.transfer( g_bleFlashErased & 0xFF);
|
||||||
|
flashDeselect();
|
||||||
|
SPI.endTransaction();
|
||||||
|
g_bleErasing = true;
|
||||||
|
// Do NOT advance g_bleFlashErased — wait for WIP confirmation next call.
|
||||||
|
}
|
||||||
|
break; // return to loop(); erase runs in HW, ring buffer fills freely
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy chunk from ring buffer into a temp page buffer
|
// Erase boundary is ahead — safe to program this page.
|
||||||
uint8_t tmp[FLASH_PAGE];
|
uint8_t tmp[FLASH_PAGE];
|
||||||
for (uint16_t i = 0; i < chunk; i++) {
|
for (uint16_t i = 0; i < chunk; i++) {
|
||||||
tmp[i] = g_bleBuf[(g_bleBufTail + i) % BLE_FLASH_BUF];
|
tmp[i] = g_bleBuf[(g_bleBufTail + i) % BLE_FLASH_BUF];
|
||||||
}
|
}
|
||||||
flashPageProgram(g_bleFlashAddr, tmp, chunk);
|
flashPageProgram(g_bleFlashAddr, tmp, chunk); // ~0.5 ms blocking
|
||||||
g_bleBufTail = (g_bleBufTail + chunk) % BLE_FLASH_BUF;
|
g_bleBufTail = (g_bleBufTail + chunk) % BLE_FLASH_BUF;
|
||||||
g_bleFlashAddr += chunk;
|
g_bleFlashAddr += chunk;
|
||||||
|
|
||||||
// Yield after each page so the rest of loop() stays responsive
|
// Pre-erase: kick off the next sector erase as soon as we start writing
|
||||||
break;
|
// the current sector so the erase (~30 ms typ) completes before we need it.
|
||||||
|
// Break immediately — never write while an async erase is in progress since
|
||||||
|
// the flash chip silently ignores page-program commands when WIP=1.
|
||||||
|
if (!g_bleErasing && g_bleFlashAddr > g_bleFlashErased - FLASH_SECTOR) {
|
||||||
|
flashWriteEnable();
|
||||||
|
SPI.beginTransaction(flashSPI);
|
||||||
|
flashSelect();
|
||||||
|
SPI.transfer(CMD_SECT_ERASE);
|
||||||
|
SPI.transfer((g_bleFlashErased >> 16) & 0xFF);
|
||||||
|
SPI.transfer((g_bleFlashErased >> 8) & 0xFF);
|
||||||
|
SPI.transfer( g_bleFlashErased & 0xFF);
|
||||||
|
flashDeselect();
|
||||||
|
SPI.endTransaction();
|
||||||
|
g_bleErasing = true;
|
||||||
|
break; // wait for erase; ring buffer fills freely in the meantime
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finalize when CMD 0x03 received and ring buffer is empty
|
// Periodic progress notification — keeps the Windows BLE stack from
|
||||||
|
// dropping the connection during long silent uploads, and gives the
|
||||||
|
// client real flash-write progress (not just BLE-send progress).
|
||||||
|
if (g_bleConnected && g_bleFlashAddr - g_bleNotifyThresh >= 4096u) {
|
||||||
|
g_bleNotifyThresh = g_bleFlashAddr;
|
||||||
|
uint32_t written = g_bleFlashAddr - g_bleFlashStart;
|
||||||
|
uint8_t stat[4] = {
|
||||||
|
(uint8_t)(written >> 24), (uint8_t)(written >> 16),
|
||||||
|
(uint8_t)(written >> 8), (uint8_t)(written)
|
||||||
|
};
|
||||||
|
audioStat.notify(stat, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize when CMD 0x03 received and ring buffer is fully drained.
|
||||||
if (g_bleFinalizing && g_bleBufHead == g_bleBufTail) {
|
if (g_bleFinalizing && g_bleBufHead == g_bleBufTail) {
|
||||||
uint32_t startAddr = g_bleFlashStart;
|
uint32_t startAddr = g_bleFlashStart;
|
||||||
uint32_t bytesStored = g_bleFlashAddr - g_bleFlashStart;
|
uint32_t bytesStored = g_bleFlashAddr - g_bleFlashStart;
|
||||||
@@ -806,6 +892,7 @@ static void bleFlashTick() {
|
|||||||
|
|
||||||
g_bleUploading = false;
|
g_bleUploading = false;
|
||||||
g_bleFinalizing = false;
|
g_bleFinalizing = false;
|
||||||
|
g_bleErasing = false; // pre-erase of unused sector, if any, will finish in HW
|
||||||
writeTrackTable();
|
writeTrackTable();
|
||||||
loadTrackTable();
|
loadTrackTable();
|
||||||
Serial.printf("BLE: Upload finalized — start=0x%08lX stored=%lu bytes\n", startAddr, bytesStored);
|
Serial.printf("BLE: Upload finalized — start=0x%08lX stored=%lu bytes\n", startAddr, bytesStored);
|
||||||
@@ -856,11 +943,15 @@ void setupBLE() {
|
|||||||
audioStat.setMaxLen(4);
|
audioStat.setMaxLen(4);
|
||||||
audioStat.begin();
|
audioStat.begin();
|
||||||
|
|
||||||
// Start advertising
|
// Start advertising.
|
||||||
|
// The 128-bit service UUID (18 bytes) plus flags + TxPower nearly fills the
|
||||||
|
// 31-byte advertisement, leaving room for only ~5 name characters — which
|
||||||
|
// truncated the name to "BabyM" over the air. Put the full name in the scan
|
||||||
|
// response (its own separate 31 bytes) so scanners see "BabyMobile" intact.
|
||||||
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
|
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
|
||||||
Bluefruit.Advertising.addTxPower();
|
Bluefruit.Advertising.addTxPower();
|
||||||
Bluefruit.Advertising.addService(audioSvc);
|
Bluefruit.Advertising.addService(audioSvc);
|
||||||
Bluefruit.Advertising.addName();
|
Bluefruit.ScanResponse.addName(); // full name here, not in the ad packet
|
||||||
Bluefruit.Advertising.restartOnDisconnect(true);
|
Bluefruit.Advertising.restartOnDisconnect(true);
|
||||||
Bluefruit.Advertising.setInterval(160, 320); // 100-200ms
|
Bluefruit.Advertising.setInterval(160, 320); // 100-200ms
|
||||||
Bluefruit.Advertising.start(0); // advertise forever
|
Bluefruit.Advertising.start(0); // advertise forever
|
||||||
@@ -1401,22 +1492,46 @@ static void serUploadTick() {
|
|||||||
Serial.println("REBOOT");
|
Serial.println("REBOOT");
|
||||||
delay(10);
|
delay(10);
|
||||||
NVIC_SystemReset();
|
NVIC_SystemReset();
|
||||||
#ifdef POC_INTERNAL_FLASH
|
|
||||||
} else if (sscanf(g_serLineBuf, "DELETE %u", &utrk) == 1) {
|
|
||||||
char fname[16];
|
|
||||||
pocFilename((uint8_t)utrk, fname);
|
|
||||||
if (InternalFS.remove(fname)) {
|
|
||||||
Serial.print("DELETED "); Serial.println(fname);
|
|
||||||
} else {
|
|
||||||
Serial.print("ERR no file "); Serial.println(fname);
|
|
||||||
}
|
|
||||||
loadTrackTable();
|
|
||||||
} else if (strcmp(g_serLineBuf, "FORMAT") == 0) {
|
} else if (strcmp(g_serLineBuf, "FORMAT") == 0) {
|
||||||
audioStop();
|
audioStop();
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
Serial.println("Formatting LittleFS...");
|
Serial.println("Formatting LittleFS...");
|
||||||
InternalFS.format();
|
InternalFS.format();
|
||||||
g_numTracks = 0;
|
g_numTracks = 0;
|
||||||
|
#else
|
||||||
|
Serial.println("Clearing track table...");
|
||||||
|
g_numTracks = 0;
|
||||||
|
g_flashNextFree = AUDIO_START_ADDR;
|
||||||
|
writeTrackTable();
|
||||||
|
loadTrackTable();
|
||||||
|
#endif
|
||||||
Serial.println("FORMAT OK");
|
Serial.println("FORMAT OK");
|
||||||
|
} else if (sscanf(g_serLineBuf, "DELETE %u", &utrk) == 1) {
|
||||||
|
#ifdef POC_INTERNAL_FLASH
|
||||||
|
char fname[16];
|
||||||
|
pocFilename((uint8_t)utrk, fname);
|
||||||
|
if (InternalFS.remove(fname)) {
|
||||||
|
Serial.print("DELETED "); Serial.println(utrk);
|
||||||
|
} else {
|
||||||
|
Serial.print("ERR no file "); Serial.println(utrk);
|
||||||
|
}
|
||||||
|
loadTrackTable();
|
||||||
|
#else
|
||||||
|
if ((uint8_t)utrk < g_numTracks) {
|
||||||
|
for (uint8_t j = (uint8_t)utrk; 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();
|
||||||
|
Serial.print("DELETED "); Serial.println(utrk);
|
||||||
|
} else {
|
||||||
|
Serial.print("ERR no track "); Serial.println(utrk);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
} else {
|
} else {
|
||||||
Serial.print("ERR bad cmd: ");
|
Serial.print("ERR bad cmd: ");
|
||||||
@@ -1430,6 +1545,7 @@ static void serUploadTick() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else { // SER_RECEIVING
|
} else { // SER_RECEIVING
|
||||||
|
g_lastActivity = millis(); // keep device awake during long transfers
|
||||||
uint8_t chunk[64];
|
uint8_t chunk[64];
|
||||||
while (Serial.available() && g_serBytesReceived < g_serBytesExpected) {
|
while (Serial.available() && g_serBytesReceived < g_serBytesExpected) {
|
||||||
int n = Serial.readBytes(chunk, min((int)sizeof(chunk),
|
int n = Serial.readBytes(chunk, min((int)sizeof(chunk),
|
||||||
@@ -1498,7 +1614,7 @@ static void serUploadTick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
if (g_playing) g_lastActivity = millis();
|
if (g_playing || g_bleUploading) g_lastActivity = millis();
|
||||||
|
|
||||||
// ---- Serial upload ----
|
// ---- Serial upload ----
|
||||||
serUploadTick();
|
serUploadTick();
|
||||||
@@ -1592,14 +1708,18 @@ void loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Auto shutoff / idle sleep ----
|
// ---- Auto shutoff / idle sleep ----
|
||||||
|
// Never sleep while BLE is connected — upload may be in progress or about
|
||||||
|
// to begin, and the CMD callback sets g_bleUploading asynchronously.
|
||||||
|
if (!g_bleConnected) {
|
||||||
if (millis() - g_lastActivity > AUTO_OFF_MS) {
|
if (millis() - g_lastActivity > AUTO_OFF_MS) {
|
||||||
enterDeepSleep();
|
enterDeepSleep();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!g_playing && !g_motorOn && !g_usbConnected && !g_bleUploading) {
|
if (!g_playing && !g_motorOn && !g_usbConnected) {
|
||||||
if (millis() - g_lastActivity > IDLE_SLEEP_MS) {
|
if (millis() - g_lastActivity > IDLE_SLEEP_MS) {
|
||||||
enterDeepSleep();
|
enterDeepSleep();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-29
@@ -7,9 +7,9 @@ Usage:
|
|||||||
|
|
||||||
Supported formats: .wav, .mp3, .ogg, .flac, .aac, .m4a, .raw (anything ffmpeg handles)
|
Supported formats: .wav, .mp3, .ogg, .flac, .aac, .m4a, .raw (anything ffmpeg handles)
|
||||||
|
|
||||||
Output format: 16-bit signed PCM WAV, 16 kHz, mono (WAV header included).
|
Output format: 8-bit unsigned PCM WAV, 8 kHz, mono (WAV header included).
|
||||||
- Already-conformant WAV files (PCM, mono, 8 or 16-bit, 8/16/32 kHz) are sent as-is.
|
- Already-conformant WAV files (PCM, mono, 8 or 16-bit, 8/16/32 kHz) are sent as-is.
|
||||||
- All other formats are converted via ffmpeg to 16-bit 16 kHz mono WAV.
|
- All other formats are converted via ffmpeg to 8-bit 8 kHz mono WAV.
|
||||||
- .raw files are treated as legacy 8-bit unsigned 16 kHz and wrapped in a WAV header.
|
- .raw files are treated as legacy 8-bit unsigned 16 kHz and wrapped in a WAV header.
|
||||||
|
|
||||||
Requires: pyserial (pip install pyserial)
|
Requires: pyserial (pip install pyserial)
|
||||||
@@ -24,7 +24,7 @@ import subprocess
|
|||||||
import serial
|
import serial
|
||||||
import time
|
import time
|
||||||
|
|
||||||
CHUNK_SIZE = 512
|
CHUNK_SIZE = 4096
|
||||||
BAUD_RATE = 115200
|
BAUD_RATE = 115200
|
||||||
TIMEOUT_S = 10.0
|
TIMEOUT_S = 10.0
|
||||||
|
|
||||||
@@ -92,10 +92,10 @@ def load_wav(path: str) -> bytes:
|
|||||||
f" ffmpeg -i \"{path}\" -ar 16000 -ac 1 -acodec pcm_s16le out.wav"
|
f" ffmpeg -i \"{path}\" -ar 16000 -ac 1 -acodec pcm_s16le out.wav"
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"Converting {os.path.basename(path)} via ffmpeg -> 16-bit 16 kHz mono WAV...")
|
print(f"Converting {os.path.basename(path)} via ffmpeg -> 8-bit 8 kHz mono WAV...")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['ffmpeg', '-y', '-i', path,
|
['ffmpeg', '-y', '-i', path,
|
||||||
'-ar', '16000', '-ac', '1', '-f', 'wav', '-acodec', 'pcm_s16le', 'pipe:1'],
|
'-ar', '8000', '-ac', '1', '-f', 'wav', '-acodec', 'pcm_u8', 'pipe:1'],
|
||||||
capture_output=True
|
capture_output=True
|
||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
@@ -141,41 +141,29 @@ def upload(port: str, track_num: int, wav: bytes) -> None:
|
|||||||
if line:
|
if line:
|
||||||
print(f" firmware: {line}")
|
print(f" firmware: {line}")
|
||||||
|
|
||||||
# Stream WAV data in chunks; poll for ERR
|
# Stream WAV data without per-chunk polling — the firmware only ever sends
|
||||||
|
# ERR mid-transfer on a flash write failure (very rare); polling after every
|
||||||
|
# chunk adds 50 ms of readline timeout overhead per packet, which for a large
|
||||||
|
# file at USB CDC speeds amounts to many minutes of wasted time.
|
||||||
sent = 0
|
sent = 0
|
||||||
err_line = None
|
ser.timeout = 0 # non-blocking reads while sending
|
||||||
ser.timeout = 0.05
|
|
||||||
while sent < total:
|
while sent < total:
|
||||||
chunk = wav[sent:sent + CHUNK_SIZE]
|
chunk = wav[sent:sent + CHUNK_SIZE]
|
||||||
ser.write(chunk)
|
ser.write(chunk)
|
||||||
sent += len(chunk)
|
sent += len(chunk)
|
||||||
pct = sent * 100 // total
|
pct = sent * 100 // total
|
||||||
print(f"\r {sent}/{total} bytes ({pct}%) ", end="", flush=True)
|
print(f"\r {sent}/{total} bytes ({pct}%) ", end="", flush=True)
|
||||||
line = ser.readline().decode(errors="replace").strip()
|
# Drain any early ERR the firmware might have sent
|
||||||
|
line = ser.read(ser.in_waiting).decode(errors="replace").strip()
|
||||||
if line.startswith("ERR"):
|
if line.startswith("ERR"):
|
||||||
err_line = line
|
print()
|
||||||
break
|
print(f"ERROR from firmware: {line}")
|
||||||
|
sys.exit(1)
|
||||||
print()
|
print()
|
||||||
ser.timeout = 2.0
|
ser.timeout = 2.0
|
||||||
|
|
||||||
if err_line:
|
# Wait for OK — allow up to 60 s for the final sector erase + flash writes
|
||||||
print(f"ERROR from firmware: {err_line}")
|
deadline = time.time() + 60.0
|
||||||
if "at=" in err_line:
|
|
||||||
accepted = int(err_line.split("at=")[1].split()[0])
|
|
||||||
safe = int(accepted * 0.9)
|
|
||||||
# estimate audio bytes (subtract header)
|
|
||||||
audio_accepted = max(0, safe - 44)
|
|
||||||
bits = struct.unpack_from('<H', wav, 34)[0] if len(wav) >= 44 else 16
|
|
||||||
rate = struct.unpack_from('<I', wav, 24)[0] if len(wav) >= 44 else 16000
|
|
||||||
max_s = audio_accepted / (bits // 8) / rate
|
|
||||||
print(f" LittleFS accepted {accepted} bytes before full.")
|
|
||||||
print(f" Safe clip length: ~{max_s:.1f}s")
|
|
||||||
print(f" Trim with: ffmpeg -i input.mp3 -t {max_s:.0f} -ar 16000 -ac 1 "
|
|
||||||
f"-acodec pcm_s16le out.wav")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Wait for OK
|
|
||||||
deadline = time.time() + TIMEOUT_S
|
|
||||||
while True:
|
while True:
|
||||||
if time.time() > deadline:
|
if time.time() > deadline:
|
||||||
sys.exit("Timed out waiting for OK")
|
sys.exit("Timed out waiting for OK")
|
||||||
|
|||||||
Reference in New Issue
Block a user