diff --git a/server/assets/donecharging.wav b/server/assets/donecharging.wav new file mode 100644 index 00000000..cf8c4a75 Binary files /dev/null and b/server/assets/donecharging.wav differ diff --git a/server/src/services/audioForwardService/chargeCompleteSound.js b/server/src/services/audioForwardService/chargeCompleteSound.js index e144758c..484645e8 100644 --- a/server/src/services/audioForwardService/chargeCompleteSound.js +++ b/server/src/services/audioForwardService/chargeCompleteSound.js @@ -4,7 +4,7 @@ const path = require('path'); const { subscribe } = require('../eventBus'); -const DONE_CHARGING_SOUND_PATH = path.resolve(__dirname, '..', '..', '..', 'public', 'donecharging.wav'); +const DONE_CHARGING_SOUND_PATH = path.resolve(__dirname, '..', '..', '..', 'assets', 'donecharging.wav'); function registerChargeCompleteSound(deps) { const { @@ -12,22 +12,18 @@ function registerChargeCompleteSound(deps) { playServerAudioFile, } = deps; - subscribe('battery.unlocked', (event = {}) => { + subscribe('battery.chargeComplete', (event = {}) => { const roverId = String(event?.payload?.roverId || '').trim(); if (!roverId) return; try { - // battery.unlocked is the existing server-side definition of "done - // charging" because it only fires after the battery manager has waited - // through its full charge-release policy. Playing from this event avoids - // duplicating charging thresholds or raw sensor-state guesses here. playServerAudioFile(roverId, DONE_CHARGING_SOUND_PATH, { source: 'charge-complete', }); - logger.info('Played charging complete sound', { roverId, filePath: DONE_CHARGING_SOUND_PATH }); + logger.info('Started charging complete sound', { roverId, filePath: DONE_CHARGING_SOUND_PATH }); } catch (err) { // A missing/offline rover or unavailable ffmpeg should not affect the - // battery manager's unlock decision. The sound is an announcement layered + // battery manager's processing. The sound is an announcement layered // on top of the state transition, so failures are logged and contained. logger.warn('Failed to play charging complete sound', { roverId, diff --git a/server/src/services/batteryManager/index.js b/server/src/services/batteryManager/index.js index 06ddd6f1..485c9038 100644 --- a/server/src/services/batteryManager/index.js +++ b/server/src/services/batteryManager/index.js @@ -30,6 +30,7 @@ function getState(roverId) { dockedCharging: false, charging: false, batteryLocked: false, + completionAnnounced: false, waitingSince: null, }); } @@ -143,6 +144,18 @@ function handleSensorEvent({ roverId, sensors, batteryState }) { (batteryState?.charge != null && fullThreshold != null && batteryState.charge >= fullThreshold) || (batteryState?.percent != null && batteryState.percent >= 0.99); + // Announce once per dock visit, independently of the charging lock. Keep the + // latch at 100% so small charge fluctuations cannot repeat the sound. + if (!onDock) state.completionAnnounced = false; + if (onDock && batteryState?.percentDisplay >= 100 && !state.completionAnnounced) { + state.completionAnnounced = true; + publishEvent({ + source: 'batteryManager', + type: 'battery.chargeComplete', + payload: { roverId, batteryState }, + }); + } + const needsCharge = state.warned; if (dockedCharging && lockable && needsCharge && !state.batteryLocked && !isFull && !waitingLongEnough) {