Compare commits

...
3 Commits
Author SHA1 Message Date
legop3 79d36cae93 update donecharging wav sound
Container image / image (push) Failing after 1m4s
2026-10-18 14:52:27 -04:00
legop3 1dcd7e5d7d add beeps for overcurrent, HELP clear, and low battery 2026-10-18 14:32:59 -04:00
legop3 b4bc99ab27 add server access node to battery embed 2026-10-18 14:22:36 -04:00
6 changed files with 35 additions and 13 deletions
Binary file not shown.
@@ -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,
@@ -4,6 +4,7 @@
const logger = require('../../globals/logger').child('batteryManager');
const { publishEvent } = require('../eventBus');
const { managerEvents, lockRover, rovers } = require('../roverManager');
const { issueCommand } = require('../commandService');
const STATES = {
NORMAL: 'normal',
@@ -29,6 +30,7 @@ function getState(roverId) {
dockedCharging: false,
charging: false,
batteryLocked: false,
completionAnnounced: false,
waitingSince: null,
});
}
@@ -61,6 +63,11 @@ function handleSensorEvent({ roverId, sensors, batteryState }) {
}
if (!state.urgent && batteryState?.urgentActive) {
state.urgent = true;
try {
issueCommand(roverId, { type: 'song', song: { notes: [{ note: 72, duration: 8 }, { note: 0, duration: 8 }, { note: 72, duration: 16 }] } });
} catch (err) {
logger.warn('Failed to play urgent battery warning', { roverId, error: err.message });
}
publishEvent({
source: 'batteryManager',
type: 'battery.urgent',
@@ -137,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) {
@@ -2,6 +2,7 @@
// Purpose: Centralizes rover battery/status snapshot formatting and embed rendering for Discord surfaces.
// Scope: Shared presentation logic for status commands and admin alert battery event posts.
const { EmbedBuilder } = require('discord.js');
const { getMode } = require('../modeManager');
function formatVoltage(voltageMv) {
if (voltageMv == null) return 'n/a';
@@ -69,6 +70,7 @@ function buildRoverStatusSnapshot(record) {
function buildBatteryStatusEmbed({ color = 0x2196f3, records = [] }) {
const embed = new EmbedBuilder().setTitle('Rover Battery Status').setColor(color).setTimestamp(new Date());
embed.addFields({ name: 'Server mode', value: getMode(), inline: true });
const snapshots = records.map((entry) => buildRoverStatusSnapshot(entry)).filter(Boolean);
if (!snapshots.length) {
embed.setDescription('No rovers online.');
@@ -400,6 +400,7 @@ function createOvercurrentProtectionService(options = {}) {
type: 'drive',
driveDirect: { left: 0, right: 0 },
});
issueAdjustedCommand(roverId, { type: 'song', song: { notes: [{ note: 76, duration: 8 }, { note: 72, duration: 8 }, { note: 67, duration: 16 }] } });
logger.warn('Stopped rover after persistent wheel overcurrent', {
roverId,
motor: stalledWheel,
@@ -2,20 +2,19 @@
// Purpose: Publishes sustained 600-series Roomba trouble as roster and alert state.
// Scope: Integrates the pure monitor with roverManager, browser alerts, and the event bus.
const roverManager = require('../roverManager');
const { issueCommand } = require('../commandService');
const { sendAlert } = require('../alertService');
const { publishEvent } = require('../eventBus');
const { REASON_LABELS, createRoverHelpMonitor } = require('./monitor');
const { createHelpHornNotifier } = require('./hornNotifier');
const logger = require('../../globals/logger').child('roverHelpService');
const HELP_ALERT_COLOR = '#ef4444';
const hornNotifier = createHelpHornNotifier({
getRover: (roverId) => roverManager.rovers.get(String(roverId)),
// commandService imports roverManager, so resolving it only when a chirp is
// actually issued avoids turning server startup order into a circular module
// dependency while retaining the established command transport.
issueCommand: (roverId, payload) => require('../commandService').issueCommand(roverId, payload),
logger: require('../../globals/logger').child('roverHelpService'),
issueCommand,
logger,
});
const monitor = createRoverHelpMonitor({
@@ -42,6 +41,11 @@ const monitor = createRoverHelpMonitor({
});
} else if (wasNeedingHelp && !needsHelp) {
hornNotifier.stop(roverId);
try {
issueCommand(roverId, { type: 'song', song: { notes: [{ note: 72, duration: 8 }, { note: 79, duration: 16 }] } });
} catch (err) {
logger.warn('Failed to play HELP-cleared confirmation', { roverId, error: err.message });
}
publishEvent({
source: 'roverHelpService',
type: 'rover.helpCleared',