feat(espnow): broadcast delivery with group-filtered routing

Send presets and select on broadcast with groups; unicast only for
per-device settings. V1 select as [preset_id, step?]. Sequence steps
use beat counts; manual presets get select each beat, auto only on
step change. Bridge downlink router, Pi envelope delivery, and tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-24 01:44:28 +12:00
parent 1a69fabd98
commit b87382d2be
35 changed files with 1802 additions and 591 deletions

View File

@@ -98,12 +98,17 @@ document.addEventListener('DOMContentLoaded', () => {
: [];
};
const postDriverSequence = async (sequence, targetMacs, delayS = 0.05) => {
const body = {
sequence,
targets: Array.isArray(targetMacs) && targetMacs.length ? [...new Set(targetMacs)] : undefined,
delay_s: delayS,
};
const postDriverSequence = async (sequence, targetMacs, delayS = 0.05, pushOptions = {}) => {
if (typeof window.postDriverSequence === 'function') {
return window.postDriverSequence(sequence, targetMacs, delayS, pushOptions);
}
const body = { sequence, delay_s: delayS };
if (pushOptions && pushOptions.unicast === true) {
body.unicast = true;
if (Array.isArray(targetMacs) && targetMacs.length) {
body.targets = [...new Set(targetMacs)];
}
}
const res = await fetch('/presets/push', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
@@ -586,26 +591,28 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
const select = {};
deviceNames.forEach((name) => {
if (name) {
select[name] = zonePresetIds.slice();
}
});
const targetMacs =
typeof window.tabsManager !== 'undefined' &&
typeof window.tabsManager.resolveTabDeviceMacs === 'function'
? await window.tabsManager.resolveTabDeviceMacs(deviceNames)
: [];
const groupIds =
typeof window.zonesManager !== 'undefined' &&
typeof window.zonesManager.effectiveGroupIdsForZonePreset === 'function'
? window.zonesManager.effectiveGroupIdsForZonePreset(zoneData)
: Array.isArray(zoneData.group_ids)
? zoneData.group_ids.map((g) => String(g).trim()).filter((g) => g.length > 0)
: [];
const sequence = [
{ v: '1', clear_presets: true, save: true },
{ v: '1', presets: wirePresets, save: true },
];
if (Object.keys(select).length) {
sequence.push({ v: '1', select });
if (groupIds.length) {
sequence[0].groups = groupIds;
sequence[1].groups = groupIds;
}
await postDriverSequence(sequence, targetMacs, 0.05);
if (deviceNames.length > 0 && zonePresetIds.length > 0) {
const sel = { v: '1', select: zonePresetIds.slice() };
if (groupIds.length) sel.groups = groupIds;
sequence.push(sel);
}
await postDriverSequence(sequence, [], 0.05, { groupIds });
} catch (error) {
console.error('Send all patterns failed:', error);
alert('Failed to send all patterns.');