mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
144 lines
4.3 KiB
JavaScript
144 lines
4.3 KiB
JavaScript
// Rover Help Horn Notifier
|
|
// Purpose: Repeats a short, disruptive locator chirp while a rover needs help.
|
|
// Scope: Uses the existing server-to-roverd horn start/stop protocol without changing roverd.
|
|
const HELP_HORN_FREQUENCY_HZ = 2000;
|
|
const HELP_HORN_DURATION_MS = 250;
|
|
const HELP_HORN_INTERVAL_MS = 5 * 1000;
|
|
const HELP_ROOMBA_NOTE = 95;
|
|
const HELP_ROOMBA_NOTE_DURATION = 16;
|
|
const HELP_ROOMBA_SONG_SLOT = 4;
|
|
|
|
function createHelpHornNotifier({
|
|
getRover,
|
|
issueCommand,
|
|
logger,
|
|
setIntervalFn = setInterval,
|
|
clearIntervalFn = clearInterval,
|
|
setTimeoutFn = setTimeout,
|
|
clearTimeoutFn = clearTimeout,
|
|
} = {}) {
|
|
const intervals = new Map();
|
|
const stopTimers = new Map();
|
|
|
|
function stopPendingHorn(roverId) {
|
|
const id = String(roverId);
|
|
const stopTimer = stopTimers.get(id);
|
|
if (stopTimer != null) clearTimeoutFn(stopTimer);
|
|
stopTimers.delete(id);
|
|
|
|
// The Roomba song is self-terminating. With no pending external-horn stop,
|
|
// there is no persistent sound owned by this notifier that needs cleanup.
|
|
if (stopTimer == null) return;
|
|
|
|
// Always send a final stop during cleanup. This ensures HELP clearing in
|
|
// the middle of a 250 ms chirp silences it immediately instead of waiting
|
|
// for a timeout that was just cancelled.
|
|
const record = getRover?.(id);
|
|
if (!record?.ws) return;
|
|
try {
|
|
issueCommand(id, { type: 'horn', horn: { action: 'stop' } });
|
|
} catch (err) {
|
|
logger?.warn?.('Failed to stop rover help horn', { roverId: id, error: err.message });
|
|
}
|
|
}
|
|
|
|
function chirp(roverId) {
|
|
const id = String(roverId);
|
|
const record = getRover?.(id);
|
|
if (!record?.ws) return false;
|
|
|
|
let sounded = false;
|
|
let externalHornStarted = false;
|
|
if (record.meta?.horn?.enabled) {
|
|
try {
|
|
issueCommand(id, {
|
|
type: 'horn',
|
|
horn: {
|
|
action: 'start',
|
|
waveform: 'saw',
|
|
freqs: [HELP_HORN_FREQUENCY_HZ],
|
|
},
|
|
});
|
|
sounded = true;
|
|
externalHornStarted = true;
|
|
} catch (err) {
|
|
logger?.warn?.('Failed to start rover help horn', { roverId: id, error: err.message });
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Roomba 600-series songs use MIDI notes and 1/64-second durations.
|
|
// Note 95 is approximately 1975.5 Hz, the closest supported pitch to the
|
|
// external 2000 Hz horn, and duration 16 matches its 250 ms pulse.
|
|
issueCommand(id, {
|
|
type: 'song',
|
|
song: {
|
|
slot: HELP_ROOMBA_SONG_SLOT,
|
|
notes: [{ note: HELP_ROOMBA_NOTE, duration: HELP_ROOMBA_NOTE_DURATION }],
|
|
},
|
|
});
|
|
sounded = true;
|
|
} catch (err) {
|
|
logger?.warn?.('Failed to play rover help song', { roverId: id, error: err.message });
|
|
}
|
|
|
|
if (externalHornStarted) {
|
|
// There can be only one pending automatic stop for a rover. Replacing an
|
|
// unexpected stale timer keeps the external pulse duration bounded; the
|
|
// independently issued Roomba song always ends itself.
|
|
const previousStop = stopTimers.get(id);
|
|
if (previousStop != null) clearTimeoutFn(previousStop);
|
|
stopTimers.set(
|
|
id,
|
|
setTimeoutFn(() => {
|
|
stopTimers.delete(id);
|
|
const current = getRover?.(id);
|
|
if (!current?.ws) return;
|
|
try {
|
|
issueCommand(id, { type: 'horn', horn: { action: 'stop' } });
|
|
} catch (err) {
|
|
logger?.warn?.('Failed to finish rover help chirp', { roverId: id, error: err.message });
|
|
}
|
|
}, HELP_HORN_DURATION_MS),
|
|
);
|
|
}
|
|
return sounded;
|
|
}
|
|
|
|
function start(roverId) {
|
|
const id = String(roverId);
|
|
if (intervals.has(id)) return;
|
|
const record = getRover?.(id);
|
|
if (!record?.ws) return;
|
|
|
|
// Sound immediately so a newly detected rover can be located without
|
|
// waiting through the first five-second interval.
|
|
chirp(id);
|
|
intervals.set(id, setIntervalFn(() => chirp(id), HELP_HORN_INTERVAL_MS));
|
|
}
|
|
|
|
function stop(roverId) {
|
|
const id = String(roverId);
|
|
const interval = intervals.get(id);
|
|
if (interval != null) clearIntervalFn(interval);
|
|
intervals.delete(id);
|
|
stopPendingHorn(id);
|
|
}
|
|
|
|
return {
|
|
chirp,
|
|
start,
|
|
stop,
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
HELP_HORN_DURATION_MS,
|
|
HELP_HORN_FREQUENCY_HZ,
|
|
HELP_HORN_INTERVAL_MS,
|
|
HELP_ROOMBA_NOTE,
|
|
HELP_ROOMBA_NOTE_DURATION,
|
|
HELP_ROOMBA_SONG_SLOT,
|
|
createHelpHornNotifier,
|
|
};
|