UNPKG

sacn

Version:

💡 🎭 Send and Receive sACN data (DMX over IP)

281 lines (246 loc) 7.56 kB
import { performance } from 'perf_hooks'; import type { Packet } from './packet'; import { Receiver } from './receiver'; import type { Payload } from './util'; /** * @deprecated CAUTION: This feature is experimental, * and has not been thoroughly tested. It may not behave * correctly. There is no guarantee that it adheres to * the E1.33 standard. */ export namespace MergingReceiver { /** See {@link Props.mode here} for docs */ export type Mode = 'HTP' | 'LTP'; export interface Props extends Receiver.Props { /** * ### Different priority * . * When merging, all senders should normally have a different * `priority`. Following this rule will prevent most of the * confusion around merging. * * 💡 _Use case: tracking-backup console._ * * ### Same priority * . * If there are 2 senders with the same `priority`, * then you need to specify the merging mode: * * - `HTP` = **H**ighest **t**akes **P**riority. This means * that the receiver will use the highest channel value from * all senders with the same `priority`. If there is a * malfunction, channels may appear to be stuck, even when * blacked-out on one console. * 💡 _Use case: {@link https://youtu.be/vygFW9FDYtM parking a channel} * or controlling {@link https://en.wiktionary.org/wiki/houselights houselights} * from a different console._ * * - `LTP` = **L**atest **t**akes **P**riority. This means that * the receiver will use the latest data that it receives from * the senders with the highest `priority`. **This options is * not recomended, because a malfunction will cause of lights * to flicker uncontrollably.** * 💡 _Use case: none._ * * ℹ️ Please refer to the README for more information. * * @default 'HTP' */ mode?: Mode; timeout?: number; } export interface EventMap extends Receiver.EventMap { changed: { universe: number; payload: Payload; }; changedValue: { universe: number; address: number; newValue: number; oldValue: number; }; changesDone: never; senderConnect: { cid: number; universe: number; firstPacket: Packet; }; senderDisconnect: { cid: number; universe: number; lastPacket: Packet; }; } export interface PacketWithTime { readonly packet: Packet; readonly timestamp: number; } export interface UniverseData { referenceData: Payload; servers: Map<string, PacketWithTime>; } export interface PreparedData { universe: number; maximumPriority: number; universeData: UniverseData; } } export declare interface MergingReceiver { on<K extends keyof MergingReceiver.EventMap>( type: K, listener: (event: MergingReceiver.EventMap[K]) => void, ): this; } export class MergingReceiver extends Receiver { private readonly mode: MergingReceiver.Mode; private readonly timeout: number; private data = new Map<number, MergingReceiver.UniverseData>(); constructor({ mode = 'HTP', timeout = 5000, ...props }: MergingReceiver.Props) { super(props); this.mode = mode; this.timeout = timeout; super.on('packet', (packet) => { const data = this.prepareData(packet); const mergedData = MergingReceiver[this.mode](data); this.handleChanges(data, mergedData); }); } private prepareData(packet: Packet): MergingReceiver.PreparedData { const currentTime = performance.now(); const universe = parseInt(packet.universe.toString(36), 10); const cid = packet.cid.toString(); // universe is unknown if (!this.data.has(universe)) { this.data.set(universe, { referenceData: {}, servers: new Map(), }); this.emit('newUniverse', { universe, firstPacket: packet, }); } const universeData = this.data.get(universe); if (!universeData) { throw new Error('[sACN] Internal Error: universeData is undefined'); } // sender is unknown for this universe if (!universeData.servers.has(cid)) { this.emit('senderConnect', { cid: packet.cid, universe, firstPacket: packet, }); } // register current package universeData.servers.set(cid, { packet, timestamp: currentTime, }); // check whether sender disconnects setTimeout(() => { if (universeData.servers.get(cid)?.timestamp === currentTime) { universeData.servers.delete(cid); this.emit('senderDisconnect', { cid: packet.cid, universe, lastPacket: packet, }); } }, this.timeout); // detect which source has the highest per-universe priority let maximumPriority = 0; for (const [, { packet: thisPacket }] of universeData.servers) { if ( thisPacket.priority > maximumPriority && thisPacket.universe === packet.universe ) { maximumPriority = thisPacket.priority; } } return { universe, maximumPriority, universeData, }; } private handleChanges( data: MergingReceiver.PreparedData, mergedData: Payload, ): void { const { referenceData } = data.universeData; // only changes let changesDetected = false; for (let ch = 1; ch <= 512; ch += 1) { if (referenceData[ch] !== mergedData[ch]) { changesDetected = true; const event: MergingReceiver.EventMap['changedValue'] = { universe: data.universe, address: ch, newValue: mergedData[ch]!, oldValue: referenceData[ch]!, }; super.emit('changedValue', event); } } if (changesDetected) { this.data.get(data.universe)!.referenceData = mergedData; const event: MergingReceiver.EventMap['changed'] = { universe: data.universe, payload: mergedData, }; super.emit('changed', event); } } public static HTP(data: MergingReceiver.PreparedData): Payload { const mergedData: Payload = {}; for (const [, { packet }] of data.universeData.servers) { if ( packet.priority === data.maximumPriority && packet.universe === data.universe ) { for (let ch = 1; ch <= 512; ch += 1) { const newValue = packet.payload[ch] || 0; if ((mergedData[ch] ?? 0) <= newValue) { mergedData[ch] = newValue; } } } } return mergedData; } /** * LTP can only operate per-universe, not per-channel. There is no * situation where LTP-per-channel would be useful. * * Therefore, this function just returns the packet with the highest * priority and the latest timestamp. */ public static LTP(data: MergingReceiver.PreparedData): Payload { let maximumTimestamp = -Infinity; for (const [, { packet, timestamp }] of data.universeData.servers) { if ( packet.priority === data.maximumPriority && packet.universe === data.universe && timestamp > maximumTimestamp ) { maximumTimestamp = timestamp; } } for (const [, { packet, timestamp }] of data.universeData.servers) { if ( packet.priority === data.maximumPriority && packet.universe === data.universe && timestamp === maximumTimestamp ) { return packet.payload; } } throw new Error('Internal error'); } }