add app, fix ble, use flash
This commit is contained in:
@@ -29,9 +29,10 @@
|
||||
* D5 (P0.05) - Button 6
|
||||
* D6 (P1.11) - Button 7
|
||||
* D7 (P1.12) - Button 8
|
||||
* D8 (P0.07) - SPI SCK → Flash pin 6
|
||||
* D9 (P0.06) - SPI MISO → Flash pin 2
|
||||
* D10 (P0.05) - SPI MOSI → Flash pin 5
|
||||
* D4 (P0.04) - Flash ~CS (PIN_FLASH_CS)
|
||||
* D8 (P0.07) - SPI SCK → Flash CLK
|
||||
* D9 (P0.06) - SPI MISO → Flash DO
|
||||
* D10 (P0.05) - SPI MOSI → Flash DI
|
||||
* A0 (P0.02) - Audio PWM output → R+C LPF → PAM8302A
|
||||
* A1 (P0.03) - Amp ~SD (HIGH=on)
|
||||
* A2 (P0.28) - Motor PWM → MOSFET gate
|
||||
@@ -55,7 +56,7 @@
|
||||
// dynamic bit-depth (8/16-bit) and sample-rate support.
|
||||
// Disable this define to revert to full SPI flash mode.
|
||||
// ============================================================
|
||||
#define POC_INTERNAL_FLASH
|
||||
//#define POC_INTERNAL_FLASH
|
||||
|
||||
#ifdef POC_INTERNAL_FLASH
|
||||
#include <Adafruit_LittleFS.h>
|
||||
@@ -407,20 +408,37 @@ void loadTrackTable() {
|
||||
((uint32_t)entry[2] << 8) | entry[3];
|
||||
g_trackLen[i] = ((uint32_t)entry[4] << 24) | ((uint32_t)entry[5] << 16) |
|
||||
((uint32_t)entry[6] << 8) | entry[7];
|
||||
}
|
||||
|
||||
// Parse WAV header from flash to get format metadata
|
||||
g_trackBits[i] = 8;
|
||||
g_trackRate[i] = SAMPLE_RATE;
|
||||
g_trackDataOff[i] = 0;
|
||||
if (g_trackLen[i] >= 44) {
|
||||
uint8_t hdr[44];
|
||||
flashReadBytes(g_trackStart[i], hdr, 44);
|
||||
if (hdr[0]=='R' && hdr[1]=='I' && hdr[2]=='F' && hdr[3]=='F') {
|
||||
uint16_t bits = (uint16_t)hdr[34] | ((uint16_t)hdr[35] << 8);
|
||||
uint32_t rate = (uint32_t)hdr[24] | ((uint32_t)hdr[25] << 8)
|
||||
| ((uint32_t)hdr[26] << 16) | ((uint32_t)hdr[27] << 24);
|
||||
if ((bits == 8 || bits == 16) && rate > 0) {
|
||||
g_trackBits[i] = (uint8_t)bits;
|
||||
g_trackRate[i] = rate;
|
||||
g_trackDataOff[i] = 44;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Serial.print(" track"); Serial.print(i);
|
||||
Serial.print(": "); Serial.print(g_trackBits[i]); Serial.print("bit ");
|
||||
Serial.print(g_trackRate[i] / 1000); Serial.print("kHz ");
|
||||
uint32_t audioBytes = g_trackLen[i] - g_trackDataOff[i];
|
||||
uint32_t bps = g_trackBits[i] / 8;
|
||||
Serial.print((audioBytes / bps) / g_trackRate[i]);
|
||||
Serial.println("s");
|
||||
}
|
||||
Serial.print("Loaded ");
|
||||
Serial.print(g_numTracks);
|
||||
Serial.println(" tracks from flash");
|
||||
for (uint8_t i = 0; i < g_numTracks; i++) {
|
||||
Serial.print(" Track ");
|
||||
Serial.print(i + 1);
|
||||
Serial.print(": addr=0x");
|
||||
Serial.print(g_trackStart[i], HEX);
|
||||
Serial.print(", ");
|
||||
Serial.print(g_trackLen[i] / SAMPLE_RATE);
|
||||
Serial.println("s");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -449,8 +467,8 @@ void writeTrackTable() {
|
||||
e[5] = (g_trackLen[i] >> 16) & 0xFF;
|
||||
e[6] = (g_trackLen[i] >> 8) & 0xFF;
|
||||
e[7] = g_trackLen[i] & 0xFF;
|
||||
e[8] = 8; // sample rate kHz
|
||||
e[9] = 8; // bits
|
||||
e[8] = (uint8_t)(g_trackRate[i] / 1000);
|
||||
e[9] = g_trackBits[i];
|
||||
e[10] = 0;
|
||||
e[11] = 0;
|
||||
}
|
||||
@@ -995,11 +1013,16 @@ uint8_t buttonRead() {
|
||||
// cooldown from expiring while the button is still held and re-triggering.
|
||||
void waitButtonRelease(uint8_t btn) {
|
||||
while (buttonRead() == btn) {
|
||||
// Keep DMA buffers fed while waiting
|
||||
if (g_playing) {
|
||||
// Keep DMA buffers fed while waiting
|
||||
for (uint8_t b = 0; b < 2; b++) {
|
||||
if (!g_bufReady[b]) audioFillBuf(b);
|
||||
}
|
||||
// Stop amp as soon as track ends — don't wait for loop() to resume
|
||||
if (g_trackDoneMs != 0 && millis() >= g_trackDoneMs) {
|
||||
g_trackDoneMs = 0;
|
||||
audioStop();
|
||||
}
|
||||
}
|
||||
}
|
||||
delay(20); // debounce after release
|
||||
@@ -1144,49 +1167,75 @@ void setup() {
|
||||
// ============================================================
|
||||
|
||||
// ============================================================
|
||||
// SERIAL UPLOAD STATE MACHINE (POC_INTERNAL_FLASH only)
|
||||
// SERIAL UPLOAD STATE MACHINE
|
||||
// Works in both POC_INTERNAL_FLASH and SPI flash modes.
|
||||
// Protocol: UPLOAD <track> <total_wav_bytes> (full WAV file, header included)
|
||||
// DUMP <track> DUMP <track>
|
||||
// p / l / u / d / r
|
||||
// DELETE <track> (POC only)
|
||||
// FORMAT (POC only)
|
||||
// ============================================================
|
||||
#ifdef POC_INTERNAL_FLASH
|
||||
enum SerUploadState { SER_IDLE, SER_RECEIVING };
|
||||
static SerUploadState g_serState = SER_IDLE;
|
||||
static char g_serLineBuf[64] = {0};
|
||||
static uint8_t g_serLineLen = 0;
|
||||
static uint8_t g_serTrack = 0;
|
||||
static SerUploadState g_serState = SER_IDLE;
|
||||
static char g_serLineBuf[64] = {0};
|
||||
static uint8_t g_serLineLen = 0;
|
||||
static uint8_t g_serTrack = 0;
|
||||
static uint32_t g_serBytesExpected = 0;
|
||||
static uint32_t g_serBytesReceived = 0;
|
||||
|
||||
#ifdef POC_INTERNAL_FLASH
|
||||
static File g_serFile(InternalFS);
|
||||
#else
|
||||
// SPI flash upload: tracks are packed sequentially starting at AUDIO_START_ADDR.
|
||||
// Uploading track 0 resets the allocation pointer.
|
||||
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
|
||||
#endif
|
||||
|
||||
static void serUploadTick() {
|
||||
if (g_serState == SER_IDLE) {
|
||||
// Accumulate characters until newline
|
||||
while (Serial.available()) {
|
||||
// Serial.print("r");
|
||||
char c = (char)Serial.read();
|
||||
if (c == '\n' || c == '\r') {
|
||||
g_serLineBuf[g_serLineLen] = '\0';
|
||||
if (g_serLineLen == 0) { g_serLineLen = 0; break; }
|
||||
// Parse: UPLOAD <track> <len>
|
||||
|
||||
unsigned int utrk = 0, ulen = 0;
|
||||
if (sscanf(g_serLineBuf, "UPLOAD %u %u", &utrk, &ulen) == 2) {
|
||||
g_serTrack = (uint8_t)utrk;
|
||||
g_serBytesExpected = (uint32_t)ulen;
|
||||
g_serBytesReceived = 0;
|
||||
|
||||
#ifdef POC_INTERNAL_FLASH
|
||||
char fname[16];
|
||||
pocFilename(g_serTrack, fname);
|
||||
if (g_serFile) g_serFile.close();
|
||||
// Remove first — FILE_O_WRITE has no truncate flag
|
||||
InternalFS.remove(fname);
|
||||
if (!g_serFile.open(fname, FILE_O_WRITE)) {
|
||||
Serial.print("ERR cannot open ");
|
||||
Serial.println(fname);
|
||||
Serial.print("ERR cannot open "); Serial.println(fname);
|
||||
} else {
|
||||
g_serState = SER_RECEIVING;
|
||||
g_usbConnected = true;
|
||||
Serial.println("READY");
|
||||
}
|
||||
#else
|
||||
// Track 0 resets flash allocation
|
||||
if (g_serTrack == 0) g_serFlashNextFree = AUDIO_START_ADDR;
|
||||
g_serFlashCurAddr = g_serFlashNextFree;
|
||||
g_trackStart[g_serTrack] = g_serFlashCurAddr;
|
||||
|
||||
// Erase first sector now; subsequent sectors erased lazily during receive
|
||||
uint32_t firstSector = (g_serFlashCurAddr / FLASH_SECTOR) * FLASH_SECTOR;
|
||||
flashEraseSector(firstSector);
|
||||
g_serFlashErasedThru = firstSector + FLASH_SECTOR - 1;
|
||||
|
||||
g_serState = SER_RECEIVING;
|
||||
g_usbConnected = true;
|
||||
Serial.println("READY");
|
||||
#endif
|
||||
} else if (sscanf(g_serLineBuf, "DUMP %u", &utrk) == 1) {
|
||||
// Hex-dump first 256 bytes of a track file
|
||||
#ifdef POC_INTERNAL_FLASH
|
||||
char fname[16];
|
||||
pocFilename((uint8_t)utrk, fname);
|
||||
File df(InternalFS);
|
||||
@@ -1197,20 +1246,40 @@ static void serUploadTick() {
|
||||
uint32_t limit = min(fsz, (uint32_t)256);
|
||||
uint32_t off = 0;
|
||||
while (off < limit) {
|
||||
int n = df.read(dbuf, min((uint32_t)sizeof(dbuf), limit - off));
|
||||
if (n <= 0) break;
|
||||
for (int j = 0; j < n; j++) {
|
||||
int rd = df.read(dbuf, min((uint32_t)sizeof(dbuf), limit - off));
|
||||
if (rd <= 0) break;
|
||||
for (int j = 0; j < rd; j++) {
|
||||
if (dbuf[j] < 0x10) Serial.print("0");
|
||||
Serial.print(dbuf[j], HEX);
|
||||
Serial.print(j % 16 == 15 || (off + j + 1) == limit ? "\n" : " ");
|
||||
}
|
||||
off += n;
|
||||
off += rd;
|
||||
}
|
||||
df.close();
|
||||
Serial.println("END");
|
||||
} else {
|
||||
Serial.println("NO FILE");
|
||||
}
|
||||
#else
|
||||
if ((uint8_t)utrk < g_numTracks) {
|
||||
Serial.print("SIZE "); Serial.println(g_trackLen[utrk]);
|
||||
uint8_t dbuf[16];
|
||||
uint32_t limit = min(g_trackLen[utrk], (uint32_t)256);
|
||||
for (uint32_t off = 0; off < limit; ) {
|
||||
uint32_t rd = min((uint32_t)sizeof(dbuf), limit - off);
|
||||
flashReadBytes(g_trackStart[utrk] + off, dbuf, rd);
|
||||
for (uint32_t j = 0; j < rd; j++) {
|
||||
if (dbuf[j] < 0x10) Serial.print("0");
|
||||
Serial.print(dbuf[j], HEX);
|
||||
Serial.print(j % 16 == 15 || (off + j + 1) == limit ? "\n" : " ");
|
||||
}
|
||||
off += rd;
|
||||
}
|
||||
Serial.println("END");
|
||||
} else {
|
||||
Serial.println("NO FILE");
|
||||
}
|
||||
#endif
|
||||
} else if (g_serLineLen == 1 && (g_serLineBuf[0] == 'u' || g_serLineBuf[0] == 'd')) {
|
||||
if (g_serLineBuf[0] == 'u') g_audioGain++;
|
||||
else if (g_audioGain > 1) g_audioGain--;
|
||||
@@ -1226,6 +1295,7 @@ static void serUploadTick() {
|
||||
Serial.println("REBOOT");
|
||||
delay(10);
|
||||
NVIC_SystemReset();
|
||||
#ifdef POC_INTERNAL_FLASH
|
||||
} else if (sscanf(g_serLineBuf, "DELETE %u", &utrk) == 1) {
|
||||
char fname[16];
|
||||
pocFilename((uint8_t)utrk, fname);
|
||||
@@ -1241,32 +1311,28 @@ static void serUploadTick() {
|
||||
InternalFS.format();
|
||||
g_numTracks = 0;
|
||||
Serial.println("FORMAT OK");
|
||||
#endif
|
||||
} else {
|
||||
Serial.print("ERR bad cmd: ");
|
||||
Serial.println(g_serLineBuf);
|
||||
}
|
||||
g_serLineLen = 0;
|
||||
// Serial.print("0");
|
||||
} else {
|
||||
if (g_serLineLen < (sizeof(g_serLineBuf) - 1)) {
|
||||
g_serLineBuf[g_serLineLen++] = c;
|
||||
}
|
||||
// Serial.print(",");
|
||||
}
|
||||
}
|
||||
} else { // SER_RECEIVING
|
||||
// Serial.print("e");
|
||||
uint8_t chunk[64];
|
||||
while (Serial.available() && g_serBytesReceived < g_serBytesExpected) {
|
||||
// Serial.print(";");
|
||||
int n = Serial.readBytes(chunk, min((int)sizeof(chunk),
|
||||
(int)(g_serBytesExpected - g_serBytesReceived)));
|
||||
if (n <= 0) break;
|
||||
|
||||
#ifdef POC_INTERNAL_FLASH
|
||||
int32_t wr = g_serFile.write(chunk, (uint16_t)n);
|
||||
if (wr != n) {
|
||||
// Write failed (LittleFS full or error) — abort immediately.
|
||||
// Do NOT loop printing errors; that fills USB CDC TX and hangs.
|
||||
g_serFile.close();
|
||||
g_usbConnected = false;
|
||||
g_serState = SER_IDLE;
|
||||
@@ -1274,14 +1340,32 @@ static void serUploadTick() {
|
||||
Serial.println(g_serBytesReceived);
|
||||
break;
|
||||
}
|
||||
g_serBytesReceived += n;
|
||||
#else
|
||||
// Lazily erase the next sector as we reach it
|
||||
uint32_t chunkEnd = g_serFlashCurAddr + (uint32_t)n - 1;
|
||||
if (chunkEnd > g_serFlashErasedThru) {
|
||||
uint32_t nextSector = (g_serFlashErasedThru + 1) / FLASH_SECTOR * FLASH_SECTOR;
|
||||
flashEraseSector(nextSector);
|
||||
g_serFlashErasedThru = nextSector + FLASH_SECTOR - 1;
|
||||
}
|
||||
// Write to flash in page-aligned chunks
|
||||
uint16_t written = 0;
|
||||
while (written < (uint16_t)n) {
|
||||
uint16_t pageOff = (uint16_t)((g_serFlashCurAddr + written) % FLASH_PAGE);
|
||||
uint16_t pageChunk = min((uint16_t)(FLASH_PAGE - pageOff), (uint16_t)(n - written));
|
||||
flashPageProgram(g_serFlashCurAddr + written, chunk + written, pageChunk);
|
||||
written += pageChunk;
|
||||
}
|
||||
g_serFlashCurAddr += (uint32_t)n;
|
||||
#endif
|
||||
g_serBytesReceived += (uint32_t)n;
|
||||
}
|
||||
|
||||
if (g_serState != SER_RECEIVING) return; // aborted in write-fail handler above
|
||||
if (g_serState != SER_RECEIVING) return;
|
||||
|
||||
if (g_serBytesReceived >= g_serBytesExpected) {
|
||||
#ifdef POC_INTERNAL_FLASH
|
||||
g_serFile.close();
|
||||
// Reopen to read the actual flushed size from LittleFS
|
||||
char fname2[16]; pocFilename(g_serTrack, fname2);
|
||||
File tmp(InternalFS);
|
||||
uint32_t fsz = 0;
|
||||
@@ -1289,22 +1373,29 @@ static void serUploadTick() {
|
||||
g_usbConnected = false;
|
||||
g_serState = SER_IDLE;
|
||||
loadTrackTable();
|
||||
Serial.print("OK ");
|
||||
Serial.print(fsz);
|
||||
Serial.print("/");
|
||||
Serial.println(g_serBytesReceived);
|
||||
Serial.print("OK "); Serial.print(fsz);
|
||||
Serial.print("/"); Serial.println(g_serBytesReceived);
|
||||
#else
|
||||
// Advance free pointer to next sector boundary
|
||||
g_serFlashNextFree = ((g_serFlashCurAddr + FLASH_SECTOR - 1) / FLASH_SECTOR) * FLASH_SECTOR;
|
||||
g_trackLen[g_serTrack] = g_serBytesReceived;
|
||||
if (g_serTrack >= g_numTracks) g_numTracks = g_serTrack + 1;
|
||||
writeTrackTable();
|
||||
loadTrackTable();
|
||||
g_usbConnected = false;
|
||||
g_serState = SER_IDLE;
|
||||
Serial.print("OK "); Serial.print(g_serBytesReceived);
|
||||
Serial.print("/"); Serial.println(g_serBytesExpected);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void loop() {
|
||||
if (g_playing) g_lastActivity = millis();
|
||||
|
||||
// ---- Serial upload (POC mode) ----
|
||||
#ifdef POC_INTERNAL_FLASH
|
||||
// ---- Serial upload ----
|
||||
serUploadTick();
|
||||
#endif
|
||||
|
||||
// ---- Refill audio buffers ----
|
||||
if (g_playing) {
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
upload_track.py — serial audio uploader for baby_mobile_v2 POC_INTERNAL_FLASH mode
|
||||
|
||||
Usage:
|
||||
python upload_track.py <port> <track_num> <file>
|
||||
|
||||
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).
|
||||
- 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.
|
||||
- .raw files are treated as legacy 8-bit unsigned 16 kHz and wrapped in a WAV header.
|
||||
|
||||
Requires: pyserial (pip install pyserial)
|
||||
ffmpeg in PATH for non-WAV/RAW files or non-conformant WAV files
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import serial
|
||||
import time
|
||||
|
||||
CHUNK_SIZE = 512
|
||||
BAUD_RATE = 115200
|
||||
TIMEOUT_S = 10.0
|
||||
|
||||
# Sample rates the firmware PWM can reproduce (32kHz carrier / (REFRESH+1))
|
||||
SUPPORTED_RATES = {8000, 16000, 32000}
|
||||
|
||||
|
||||
def _make_wav_header(num_samples: int, sample_rate: int, bits: int) -> bytes:
|
||||
"""Build a minimal 44-byte PCM WAV header."""
|
||||
num_channels = 1
|
||||
byte_rate = sample_rate * num_channels * bits // 8
|
||||
block_align = num_channels * bits // 8
|
||||
data_size = num_samples * block_align
|
||||
chunk_size = 36 + data_size
|
||||
return struct.pack('<4sI4s4sIHHIIHH4sI',
|
||||
b'RIFF', chunk_size, b'WAVE',
|
||||
b'fmt ', 16, 1, num_channels, sample_rate,
|
||||
byte_rate, block_align, bits,
|
||||
b'data', data_size)
|
||||
|
||||
|
||||
def load_wav(path: str) -> bytes:
|
||||
"""Return full WAV bytes (header + PCM data) ready to upload."""
|
||||
ext = os.path.splitext(path)[1].lower()
|
||||
|
||||
# ── Legacy .raw: 8-bit unsigned 16 kHz mono ─────────────────────────────
|
||||
if ext == '.raw':
|
||||
with open(path, 'rb') as f:
|
||||
pcm = f.read()
|
||||
print(f"RAW file: wrapping {len(pcm):,} bytes as 8-bit 16 kHz mono WAV")
|
||||
header = _make_wav_header(len(pcm), 16000, 8)
|
||||
return header + pcm
|
||||
|
||||
# ── WAV: check if already conformant ────────────────────────────────────
|
||||
if ext == '.wav':
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
if data[:4] == b'RIFF' and len(data) >= 44:
|
||||
try:
|
||||
audio_format = struct.unpack_from('<H', data, 20)[0]
|
||||
num_channels = struct.unpack_from('<H', data, 22)[0]
|
||||
sample_rate = struct.unpack_from('<I', data, 24)[0]
|
||||
bits_per_samp = struct.unpack_from('<H', data, 34)[0]
|
||||
conformant = (
|
||||
audio_format == 1 and # PCM
|
||||
num_channels == 1 and # mono
|
||||
bits_per_samp in (8, 16) and
|
||||
sample_rate in SUPPORTED_RATES
|
||||
)
|
||||
except Exception:
|
||||
conformant = False
|
||||
|
||||
if conformant:
|
||||
duration = (len(data) - 44) / (bits_per_samp // 8) / sample_rate
|
||||
print(f"WAV already conformant: {bits_per_samp}-bit {sample_rate // 1000}kHz "
|
||||
f"mono — {duration:.1f}s")
|
||||
return data # send as-is, header included
|
||||
print("WAV needs conversion — running ffmpeg")
|
||||
# fall through to ffmpeg
|
||||
|
||||
# ── All other formats (and non-conformant WAV): convert via ffmpeg ───────
|
||||
if not shutil.which('ffmpeg'):
|
||||
sys.exit(
|
||||
"ERROR: ffmpeg not found. Install it or convert manually:\n"
|
||||
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...")
|
||||
result = subprocess.run(
|
||||
['ffmpeg', '-y', '-i', path,
|
||||
'-ar', '16000', '-ac', '1', '-f', 'wav', '-acodec', 'pcm_s16le', 'pipe:1'],
|
||||
capture_output=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
sys.exit(f"ERROR: ffmpeg failed:\n{result.stderr.decode(errors='replace')[-400:]}")
|
||||
|
||||
wav = result.stdout
|
||||
if len(wav) >= 44:
|
||||
bits = struct.unpack_from('<H', wav, 34)[0]
|
||||
rate = struct.unpack_from('<I', wav, 24)[0]
|
||||
audio_bytes = len(wav) - 44
|
||||
duration = audio_bytes / (bits // 8) / rate
|
||||
print(f" Converted: {audio_bytes:,} audio bytes ({duration:.1f}s), "
|
||||
f"{bits}-bit {rate // 1000}kHz")
|
||||
return wav
|
||||
|
||||
|
||||
def upload(port: str, track_num: int, wav: bytes) -> None:
|
||||
total = len(wav)
|
||||
print(f"Uploading {total} bytes as track {track_num} via {port} ...")
|
||||
|
||||
with serial.Serial(port, BAUD_RATE, timeout=2.0,
|
||||
dsrdtr=False, rtscts=False) as ser:
|
||||
time.sleep(0.5)
|
||||
ser.reset_input_buffer()
|
||||
ser.write(b"\n")
|
||||
ser.flush()
|
||||
time.sleep(0.1)
|
||||
ser.reset_input_buffer()
|
||||
|
||||
# Send UPLOAD command
|
||||
cmd = f"UPLOAD {track_num} {total}\n"
|
||||
ser.write(cmd.encode())
|
||||
ser.flush()
|
||||
|
||||
# Wait for READY
|
||||
deadline = time.time() + TIMEOUT_S
|
||||
while True:
|
||||
if time.time() > deadline:
|
||||
sys.exit("Timed out waiting for READY")
|
||||
line = ser.readline().decode(errors="replace").strip()
|
||||
if line == "READY":
|
||||
break
|
||||
if line:
|
||||
print(f" firmware: {line}")
|
||||
|
||||
# Stream WAV data in chunks; poll for ERR
|
||||
sent = 0
|
||||
err_line = None
|
||||
ser.timeout = 0.05
|
||||
while sent < total:
|
||||
chunk = wav[sent:sent + CHUNK_SIZE]
|
||||
ser.write(chunk)
|
||||
sent += len(chunk)
|
||||
pct = sent * 100 // total
|
||||
print(f"\r {sent}/{total} bytes ({pct}%) ", end="", flush=True)
|
||||
line = ser.readline().decode(errors="replace").strip()
|
||||
if line.startswith("ERR"):
|
||||
err_line = line
|
||||
break
|
||||
print()
|
||||
ser.timeout = 2.0
|
||||
|
||||
if err_line:
|
||||
print(f"ERROR from firmware: {err_line}")
|
||||
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:
|
||||
if time.time() > deadline:
|
||||
sys.exit("Timed out waiting for OK")
|
||||
line = ser.readline().decode(errors="replace").strip()
|
||||
if line.startswith("OK"):
|
||||
print(f"Upload done: {line}")
|
||||
break
|
||||
if line.startswith("ERR"):
|
||||
print(f"ERROR from firmware: {line}")
|
||||
sys.exit(1)
|
||||
if line:
|
||||
print(f" firmware: {line}")
|
||||
|
||||
# Verify: hex-dump first 256 bytes and check WAV header is intact
|
||||
print("Verifying...")
|
||||
ser.reset_input_buffer()
|
||||
ser.write(f"DUMP {track_num}\n".encode())
|
||||
ser.flush()
|
||||
|
||||
deadline = time.time() + TIMEOUT_S
|
||||
fw_size = None
|
||||
fw_bytes = bytearray()
|
||||
while True:
|
||||
if time.time() > deadline:
|
||||
print("WARNING: timed out waiting for DUMP response")
|
||||
break
|
||||
line = ser.readline().decode(errors="replace").strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("SIZE"):
|
||||
fw_size = int(line.split()[1])
|
||||
elif line == "END":
|
||||
break
|
||||
elif line == "NO FILE":
|
||||
print("ERROR: firmware says file does not exist!")
|
||||
break
|
||||
else:
|
||||
for tok in line.split():
|
||||
try:
|
||||
fw_bytes.append(int(tok, 16))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if fw_size is not None:
|
||||
print(f" Firmware file size: {fw_size} bytes")
|
||||
if fw_size == 0:
|
||||
print(" ERROR: file is empty — write failed (LittleFS full?)")
|
||||
elif fw_size < total:
|
||||
print(f" WARNING: only {fw_size}/{total} bytes written (LittleFS full?)")
|
||||
else:
|
||||
print(f" Size OK: {fw_size}/{total}")
|
||||
|
||||
# Compare first 256 bytes (includes WAV header — confirms format metadata made it)
|
||||
compare_len = min(len(fw_bytes), len(wav), 256)
|
||||
if compare_len > 0:
|
||||
mismatches = sum(1 for i in range(compare_len) if fw_bytes[i] != wav[i])
|
||||
if mismatches == 0:
|
||||
print(f" Data OK: first {compare_len} bytes match (WAV header intact)")
|
||||
else:
|
||||
print(f" ERROR: {mismatches}/{compare_len} byte mismatches in first {compare_len} bytes")
|
||||
print(f" Expected: {' '.join(f'{b:02X}' for b in wav[:16])}")
|
||||
print(f" Got: {' '.join(f'{b:02X}' for b in fw_bytes[:16])}")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 4:
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
port = sys.argv[1]
|
||||
track_num = int(sys.argv[2])
|
||||
path = sys.argv[3]
|
||||
|
||||
if not 0 <= track_num < 32:
|
||||
sys.exit("track_num must be 0–31")
|
||||
|
||||
wav = load_wav(path)
|
||||
if not wav:
|
||||
sys.exit("File is empty after conversion")
|
||||
|
||||
upload(port, track_num, wav)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user