improved web ui, upload is sorta better, 5sec advertise btn

This commit is contained in:
zyphlar
2026-07-04 03:28:14 -07:00
parent 8cba579780
commit d3bd7b71b9
8 changed files with 458 additions and 79 deletions
+143 -16
View File
@@ -584,10 +584,14 @@ void ble_connect_cb(uint16_t conn_handle) {
// Request fast connection parameters and maximum throughput features.
// The central may accept, renegotiate, or ignore these — all safe.
// NOTE: do NOT call requestMtuExchange() here. The GATT client (browser)
// initiates MTU exchange automatically during connection setup. A second
// ATT_EXCHANGE_MTU_REQ from the peripheral is a protocol violation (only
// one exchange per connection) and leaves Chrome's ATT layer in a stale
// "operation in progress" state, blocking all subsequent GATT writes.
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
}
@@ -601,6 +605,67 @@ void ble_disconnect_cb(uint16_t conn_handle, uint8_t reason) {
g_bleErasing = false; // stop tracking the in-progress erase; it will finish in HW
}
// ============================================================
// GLOBAL VOLUME
// ============================================================
// Software gain in Q8 fixed point: 256 = unity (1.0x), 128 = half (-6 dB),
// 512 = 2x. Lets us ATTENUATE below unity (smaller PWM swing → lower amp output
// → less supply current, which avoids the regulator brown-out at high volume).
// Default below unity because the fixed-gain amp is loud at full scale.
// Set via BLE CMD 0x08 (persisted) or 'u'/'d' / "VOL <pct>" over serial.
#define GAIN_UNITY 256
#define GAIN_MIN 8 // ~0.03x
#define GAIN_MAX 1024 // 4x
#define VOL_FILE "/volume"
static uint16_t g_audioGainQ8 = 128; // 0.5x (overridden by saved value on boot)
static uint8_t gainToPercent() {
uint32_t p = ((uint32_t)g_audioGainQ8 * 100u) / GAIN_UNITY;
return (uint8_t)min(p, (uint32_t)255);
}
static void setGainPercent(uint8_t pct) {
uint32_t q = ((uint32_t)pct * GAIN_UNITY) / 100u;
if (q < GAIN_MIN) q = GAIN_MIN;
if (q > GAIN_MAX) q = GAIN_MAX;
g_audioGainQ8 = (uint16_t)q;
}
static void saveVolume() {
#ifdef POC_INTERNAL_FLASH
InternalFS.remove(VOL_FILE); // FILE_O_WRITE has no truncate
File f(InternalFS);
if (f.open(VOL_FILE, FILE_O_WRITE)) {
uint8_t b[2] = { (uint8_t)(g_audioGainQ8 & 0xFF), (uint8_t)(g_audioGainQ8 >> 8) };
f.write(b, 2);
f.close();
}
#endif
}
static void loadVolume() {
#ifdef POC_INTERNAL_FLASH
File f(InternalFS);
if (f.open(VOL_FILE, FILE_O_READ)) {
uint8_t b[2];
if (f.read(b, 2) == 2) {
uint16_t v = (uint16_t)b[0] | ((uint16_t)b[1] << 8);
if (v >= GAIN_MIN && v <= GAIN_MAX) g_audioGainQ8 = v;
}
f.close();
}
Serial.print("Volume: "); Serial.print(gainToPercent()); Serial.println("%");
#endif
}
// Report current volume over BLE: [0xC0, percent, 0, 0]. Tag 0xC0 has bit7 set
// but (0xC0 & 0x7F)=64 ≥ MAX_TRACKS, so the host won't mistake it for a list entry.
static void notifyVolume() {
uint8_t pkt[4] = { 0xC0, gainToPercent(), 0, 0 };
audioStat.write(pkt, 4);
audioStat.notify(pkt, 4);
}
// BLE command characteristic: receives commands
// CMD 0x01 [num_tracks] [track_entries...] = write track table
// CMD 0x02 [track_num] = start writing audio to track slot
@@ -610,6 +675,8 @@ void ble_disconnect_cb(uint16_t conn_handle, uint8_t reason) {
// CMD 0x06 = list tracks → audioStat notifications:
// [0x80|idx, bits, rate_kHz, dur_s] per track, then [0xFF, count, 0, 0]
// CMD 0x07 [track_num] = delete track → audioStat: [0xD0, idx, ok, 0]
// CMD 0x08 [percent] = set global volume (persisted) → audioStat: [0xC0, pct, 0, 0]
// CMD 0x09 = get global volume → audioStat: [0xC0, pct, 0, 0]
void audioCmd_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
uint8_t* data, uint16_t len) {
if (len < 1) return;
@@ -763,6 +830,19 @@ void audioCmd_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
Serial.println(ok ? " OK" : " FAILED");
}
break;
case 0x08: // Set global volume (percent of unity), persist, and echo back
if (len >= 2) {
setGainPercent(data[1]);
saveVolume();
notifyVolume();
Serial.print("BLE: Volume "); Serial.print(gainToPercent()); Serial.println("%");
}
break;
case 0x09: // Get global volume
notifyVolume();
break;
}
}
@@ -923,8 +1003,8 @@ void setupBLE() {
// Audio service
audioSvc.begin();
// Command characteristic (write)
audioCmd.setProperties(CHR_PROPS_WRITE);
// Command characteristic (write without response — avoids "GATT already in progress" errors)
audioCmd.setProperties(CHR_PROPS_WRITE_WO_RESP);
audioCmd.setPermission(SECMODE_OPEN, SECMODE_OPEN);
audioCmd.setMaxLen(240);
audioCmd.setWriteCallback(audioCmd_write_cb);
@@ -943,7 +1023,7 @@ void setupBLE() {
audioStat.setMaxLen(4);
audioStat.begin();
// Start advertising.
// Configure advertising packet.
// 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
@@ -952,11 +1032,25 @@ void setupBLE() {
Bluefruit.Advertising.addTxPower();
Bluefruit.Advertising.addService(audioSvc);
Bluefruit.ScanResponse.addName(); // full name here, not in the ad packet
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.restartOnDisconnect(false); // don't auto-re-advertise after disconnect
Bluefruit.Advertising.setInterval(160, 320); // 100-200ms
Bluefruit.Advertising.start(0); // advertise forever
// Do NOT start advertising here — user must hold BTN1 for 5 s to enable it.
Serial.println("BLE advertising as 'BabyMobile'");
Serial.println("BLE ready (hold BTN1 for 5 s to advertise)");
}
// Start BLE advertising for up to 60 seconds.
// Called when the user holds BTN1 for 5 s. Advertising stops automatically
// after the timeout, or immediately when a connection is made.
#define BLE_ADV_TIMEOUT_S 60
static void startBLEAdvertising() {
if (Bluefruit.Advertising.isRunning()) {
Serial.println("BLE already advertising");
return;
}
Bluefruit.Advertising.start(BLE_ADV_TIMEOUT_S);
Serial.println("BLE advertising started (60 s)");
}
@@ -976,8 +1070,8 @@ void setupBLE() {
#define PWM_COUNTERTOP 500 // 16 MHz / 500 = 32 kHz carrier
#define PWM_SILENCE 250 // midpoint of 0500 duty-cycle range
// Software gain: 1=unity, 2=2x, etc. Adjustable via 'u'/'d' serial.
static uint8_t g_audioGain = 1;
// Audio gain (g_audioGainQ8 + GAIN_* + volume persistence) is defined earlier,
// before the BLE command callback that sets it. See "GLOBAL VOLUME" section.
// Read PCM into g_pwmBuf[b], pad tail with silence.
// Supports 8-bit unsigned and 16-bit signed WAV; format read from g_trackBits[].
@@ -1007,7 +1101,7 @@ static void audioFillBuf(uint8_t b) {
}
for (uint32_t i = 0; i < samples; i++) {
int16_t s = (int16_t)((uint16_t)raw[i * 2] | ((uint16_t)raw[i * 2 + 1] << 8));
int32_t sv = (int32_t)s * g_audioGain;
int32_t sv = ((int32_t)s * g_audioGainQ8) >> 8;
if (sv > 32767) sv = 32767;
if (sv < -32768) sv = -32768;
g_pwmBuf[b][i] = (uint16_t)(((uint32_t)(sv + 32768)) * PWM_COUNTERTOP / 65536);
@@ -1023,8 +1117,7 @@ static void audioFillBuf(uint8_t b) {
g_nextReadAddr += toReadBytes;
}
for (uint32_t i = 0; i < samples; i++) {
int16_t s = (int16_t)pcm[i] - 128;
s *= g_audioGain;
int32_t s = (((int32_t)pcm[i] - 128) * g_audioGainQ8) >> 8;
if (s > 127) s = 127;
if (s < -128) s = -128;
g_pwmBuf[b][i] = (uint16_t)((uint8_t)(s + 128)) * PWM_COUNTERTOP / 256;
@@ -1305,6 +1398,7 @@ void setup() {
#ifdef POC_INTERNAL_FLASH
InternalFS.begin();
Serial.println("InternalFS mounted");
loadVolume(); // restore persisted global volume
#else
pinMode(PIN_FLASH_CS, OUTPUT);
digitalWrite(PIN_FLASH_CS, HIGH);
@@ -1478,9 +1572,22 @@ static void serUploadTick() {
}
#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--;
Serial.print("GAIN "); Serial.println(g_audioGain);
// Nudge gain by 0.125 (32 in Q8), clamped to [GAIN_MIN, GAIN_MAX].
if (g_serLineBuf[0] == 'u') {
g_audioGainQ8 = (g_audioGainQ8 + 32 > GAIN_MAX) ? GAIN_MAX : g_audioGainQ8 + 32;
} else {
g_audioGainQ8 = (g_audioGainQ8 < GAIN_MIN + 32) ? GAIN_MIN : g_audioGainQ8 - 32;
}
Serial.print("GAIN "); Serial.print((g_audioGainQ8 * 100) / GAIN_UNITY);
Serial.println("%");
} else if (sscanf(g_serLineBuf, "VOL %u", &utrk) == 1) {
// Set absolute volume as a percent of unity (0400%).
uint32_t q = ((uint32_t)utrk * GAIN_UNITY) / 100u;
if (q < GAIN_MIN) q = GAIN_MIN;
if (q > GAIN_MAX) q = GAIN_MAX;
g_audioGainQ8 = (uint16_t)q;
Serial.print("GAIN "); Serial.print((g_audioGainQ8 * 100) / GAIN_UNITY);
Serial.println("%");
} else if (g_serLineLen == 1 && g_serLineBuf[0] == 'p') {
if (g_playing) { audioStop(); Serial.println("STOP"); }
else if (g_numTracks > 0) { audioStart(g_currentTrack); Serial.println("PLAY"); }
@@ -1653,7 +1760,27 @@ void loop() {
g_lastActivity = millis();
switch (btn) {
case 1: // Play / Pause
case 1: { // Short press: Play/Pause. Hold 5 s: start BLE advertising.
unsigned long pressStart = millis();
bool longPress = false;
while (buttonRead() == 1) {
if (millis() - pressStart >= 5000UL) { longPress = true; break; }
if (g_playing) {
for (uint8_t b = 0; b < 2; b++) if (!g_bufReady[b]) audioFillBuf(b);
if (g_trackDoneMs != 0 && millis() >= g_trackDoneMs) {
g_trackDoneMs = 0; audioStop();
}
}
}
if (longPress) {
startBLEAdvertising();
waitButtonRelease(1);
} else {
if (g_playing) { audioStop(); }
else if (g_numTracks > 0) { audioStart(g_currentTrack); }
}
break;
}
case 2:
case 3:
case 4: