mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
also beep roomba at the same time
This commit is contained in:
@@ -4,6 +4,9 @@
|
|||||||
const HELP_HORN_FREQUENCY_HZ = 2000;
|
const HELP_HORN_FREQUENCY_HZ = 2000;
|
||||||
const HELP_HORN_DURATION_MS = 250;
|
const HELP_HORN_DURATION_MS = 250;
|
||||||
const HELP_HORN_INTERVAL_MS = 5 * 1000;
|
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({
|
function createHelpHornNotifier({
|
||||||
getRover,
|
getRover,
|
||||||
@@ -23,6 +26,10 @@ function createHelpHornNotifier({
|
|||||||
if (stopTimer != null) clearTimeoutFn(stopTimer);
|
if (stopTimer != null) clearTimeoutFn(stopTimer);
|
||||||
stopTimers.delete(id);
|
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
|
// 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
|
// the middle of a 250 ms chirp silences it immediately instead of waiting
|
||||||
// for a timeout that was just cancelled.
|
// for a timeout that was just cancelled.
|
||||||
@@ -38,48 +45,71 @@ function createHelpHornNotifier({
|
|||||||
function chirp(roverId) {
|
function chirp(roverId) {
|
||||||
const id = String(roverId);
|
const id = String(roverId);
|
||||||
const record = getRover?.(id);
|
const record = getRover?.(id);
|
||||||
if (!record?.ws || !record?.meta?.horn?.enabled) return false;
|
if (!record?.ws) return false;
|
||||||
|
|
||||||
try {
|
let sounded = false;
|
||||||
issueCommand(id, {
|
let externalHornStarted = false;
|
||||||
type: 'horn',
|
if (record.meta?.horn?.enabled) {
|
||||||
horn: {
|
try {
|
||||||
action: 'start',
|
issueCommand(id, {
|
||||||
waveform: 'saw',
|
type: 'horn',
|
||||||
freqs: [HELP_HORN_FREQUENCY_HZ],
|
horn: {
|
||||||
},
|
action: 'start',
|
||||||
});
|
waveform: 'saw',
|
||||||
} catch (err) {
|
freqs: [HELP_HORN_FREQUENCY_HZ],
|
||||||
logger?.warn?.('Failed to start rover help horn', { roverId: id, error: err.message });
|
},
|
||||||
return false;
|
});
|
||||||
|
sounded = true;
|
||||||
|
externalHornStarted = true;
|
||||||
|
} catch (err) {
|
||||||
|
logger?.warn?.('Failed to start rover help horn', { roverId: id, error: err.message });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// There can be only one pending automatic stop for a rover. Replacing an
|
try {
|
||||||
// unexpected stale timer keeps the pulse duration bounded even if chirp is
|
// Roomba 600-series songs use MIDI notes and 1/64-second durations.
|
||||||
// called manually in addition to its normal five-second interval.
|
// Note 95 is approximately 1975.5 Hz, the closest supported pitch to the
|
||||||
const previousStop = stopTimers.get(id);
|
// external 2000 Hz horn, and duration 16 matches its 250 ms pulse.
|
||||||
if (previousStop != null) clearTimeoutFn(previousStop);
|
issueCommand(id, {
|
||||||
stopTimers.set(
|
type: 'song',
|
||||||
id,
|
song: {
|
||||||
setTimeoutFn(() => {
|
slot: HELP_ROOMBA_SONG_SLOT,
|
||||||
stopTimers.delete(id);
|
notes: [{ note: HELP_ROOMBA_NOTE, duration: HELP_ROOMBA_NOTE_DURATION }],
|
||||||
const current = getRover?.(id);
|
},
|
||||||
if (!current?.ws) return;
|
});
|
||||||
try {
|
sounded = true;
|
||||||
issueCommand(id, { type: 'horn', horn: { action: 'stop' } });
|
} catch (err) {
|
||||||
} catch (err) {
|
logger?.warn?.('Failed to play rover help song', { roverId: id, error: err.message });
|
||||||
logger?.warn?.('Failed to finish rover help chirp', { roverId: id, error: err.message });
|
}
|
||||||
}
|
|
||||||
}, HELP_HORN_DURATION_MS),
|
if (externalHornStarted) {
|
||||||
);
|
// There can be only one pending automatic stop for a rover. Replacing an
|
||||||
return true;
|
// 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) {
|
function start(roverId) {
|
||||||
const id = String(roverId);
|
const id = String(roverId);
|
||||||
if (intervals.has(id)) return;
|
if (intervals.has(id)) return;
|
||||||
const record = getRover?.(id);
|
const record = getRover?.(id);
|
||||||
if (!record?.ws || !record?.meta?.horn?.enabled) return;
|
if (!record?.ws) return;
|
||||||
|
|
||||||
// Sound immediately so a newly detected rover can be located without
|
// Sound immediately so a newly detected rover can be located without
|
||||||
// waiting through the first five-second interval.
|
// waiting through the first five-second interval.
|
||||||
@@ -106,5 +136,8 @@ module.exports = {
|
|||||||
HELP_HORN_DURATION_MS,
|
HELP_HORN_DURATION_MS,
|
||||||
HELP_HORN_FREQUENCY_HZ,
|
HELP_HORN_FREQUENCY_HZ,
|
||||||
HELP_HORN_INTERVAL_MS,
|
HELP_HORN_INTERVAL_MS,
|
||||||
|
HELP_ROOMBA_NOTE,
|
||||||
|
HELP_ROOMBA_NOTE_DURATION,
|
||||||
|
HELP_ROOMBA_SONG_SLOT,
|
||||||
createHelpHornNotifier,
|
createHelpHornNotifier,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ const {
|
|||||||
HELP_HORN_DURATION_MS,
|
HELP_HORN_DURATION_MS,
|
||||||
HELP_HORN_FREQUENCY_HZ,
|
HELP_HORN_FREQUENCY_HZ,
|
||||||
HELP_HORN_INTERVAL_MS,
|
HELP_HORN_INTERVAL_MS,
|
||||||
|
HELP_ROOMBA_NOTE,
|
||||||
|
HELP_ROOMBA_NOTE_DURATION,
|
||||||
|
HELP_ROOMBA_SONG_SLOT,
|
||||||
createHelpHornNotifier,
|
createHelpHornNotifier,
|
||||||
} = require('./hornNotifier');
|
} = require('./hornNotifier');
|
||||||
|
|
||||||
@@ -35,17 +38,29 @@ function createHarness({ enabled = true } = {}) {
|
|||||||
return { clearedIntervals, clearedTimeouts, commands, intervals, notifier, timeouts };
|
return { clearedIntervals, clearedTimeouts, commands, intervals, notifier, timeouts };
|
||||||
}
|
}
|
||||||
|
|
||||||
test('starts an immediate 2000 Hz saw chirp and schedules the agreed cadence', () => {
|
test('starts matching external and Roomba chirps and schedules the agreed cadence', () => {
|
||||||
const harness = createHarness();
|
const harness = createHarness();
|
||||||
harness.notifier.start('red');
|
harness.notifier.start('red');
|
||||||
|
|
||||||
assert.deepEqual(harness.commands, [{
|
assert.deepEqual(harness.commands, [
|
||||||
roverId: 'red',
|
{
|
||||||
payload: {
|
roverId: 'red',
|
||||||
type: 'horn',
|
payload: {
|
||||||
horn: { action: 'start', waveform: 'saw', freqs: [HELP_HORN_FREQUENCY_HZ] },
|
type: 'horn',
|
||||||
|
horn: { action: 'start', waveform: 'saw', freqs: [HELP_HORN_FREQUENCY_HZ] },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}]);
|
{
|
||||||
|
roverId: 'red',
|
||||||
|
payload: {
|
||||||
|
type: 'song',
|
||||||
|
song: {
|
||||||
|
slot: HELP_ROOMBA_SONG_SLOT,
|
||||||
|
notes: [{ note: HELP_ROOMBA_NOTE, duration: HELP_ROOMBA_NOTE_DURATION }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
assert.equal(Array.from(harness.intervals.values())[0].ms, HELP_HORN_INTERVAL_MS);
|
assert.equal(Array.from(harness.intervals.values())[0].ms, HELP_HORN_INTERVAL_MS);
|
||||||
assert.equal(Array.from(harness.timeouts.values())[0].ms, HELP_HORN_DURATION_MS);
|
assert.equal(Array.from(harness.timeouts.values())[0].ms, HELP_HORN_DURATION_MS);
|
||||||
|
|
||||||
@@ -65,10 +80,19 @@ test('stop cancels cadence and pending pulse before issuing a final horn stop',
|
|||||||
assert.equal(harness.commands.at(-1).payload.horn.action, 'stop');
|
assert.equal(harness.commands.at(-1).payload.horn.action, 'stop');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not schedule chirps for a rover without an enabled horn', () => {
|
test('still schedules the Roomba song when the external horn is disabled', () => {
|
||||||
const harness = createHarness({ enabled: false });
|
const harness = createHarness({ enabled: false });
|
||||||
harness.notifier.start('green');
|
harness.notifier.start('green');
|
||||||
assert.equal(harness.commands.length, 0);
|
assert.deepEqual(harness.commands, [{
|
||||||
assert.equal(harness.intervals.size, 0);
|
roverId: 'green',
|
||||||
|
payload: {
|
||||||
|
type: 'song',
|
||||||
|
song: {
|
||||||
|
slot: HELP_ROOMBA_SONG_SLOT,
|
||||||
|
notes: [{ note: HELP_ROOMBA_NOTE, duration: HELP_ROOMBA_NOTE_DURATION }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}]);
|
||||||
|
assert.equal(harness.intervals.size, 1);
|
||||||
assert.equal(harness.timeouts.size, 0);
|
assert.equal(harness.timeouts.size, 0);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user