improved web ui, upload is sorta better, 5sec advertise btn
This commit is contained in:
@@ -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 0–500 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 (0–400%).
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user