UNPKG

htd-lync

Version:

This library is meant to be used to interact with an HTD Lync 12

329 lines (328 loc) 11.4 kB
import Lookup from "./Lookup.js"; // Command: Header, IsVolume, Zone, Mode, Func|PartyInput, Checksum export const Header = 0x02; export const IsVolume = { No: 0x00, Yes: 0x01 }; export const Zone = { Broadcast: 0x00, _01: 0x01, _02: 0x02, _03: 0x03, _04: 0x04, _05: 0x05, _06: 0x06, _07: 0x07, _08: 0x08, _09: 0x09, _10: 0x0A, _11: 0x0B, _12: 0x0C }; export const Command = { Set_MP3_Repeat: 0x01, Common: 0x04, Get_Status_All_Zones: 0x05, Set_Name_Zone: 0x06, Set_Name_Source: 0x07, Id: 0x08, Recall_File: 0x0A, Save_File: 0x0B, Get_Status_All: 0x0C, Get_Zone_Name: 0x0D, Get_Zone_Source_Name: 0x0E, Firmware: 0x0F, // V3 Get_Status_Zones: 0x11, // Undocumented Set_Volume: 0x15, Set_Balance: 0x16, Set_Treble: 0x17, Set_Bass: 0x18, Echo: 0x19, Set_Name_Default: 0x1C, Set_Audio_Default: 0x1E, }; export const Data = { Off: 0x00, Shuffle: 0x02, // V3 MP3_FF: 0x0A, MP3_Play: 0x0B, MP3_Back: 0x0C, MP3_Stop: 0x0D, Mute_On: 0x1E, Mute_Off: 0x1F, All_Power_On: 0x55, All_Power_Off: 0x56, Zone_Power_On: 0x57, Zone_Power_Off: 0x58, DND_On: 0x59, DND_Off: 0x5A, On: 0xFF }; export const Source = { _01: 0x10, _02: 0x11, _03: 0x12, _04: 0x13, _05: 0x14, _06: 0x15, _07: 0x16, _08: 0x17, _09: 0x18, _10: 0x19, _11: 0x1A, _12: 0x1B, _13: 0x63, _14: 0x64, _15: 0x65, _16: 0x66, _17: 0x67, _18: 0x68 }; export const PartySource = { _01: 0x36, _02: 0x37, _03: 0x38, _04: 0x39, _05: 0x3A, _06: 0x3B, _07: 0x3C, _08: 0x3D, _09: 0x3E, _10: 0x3F, _11: 0x40, _12: 0x41, _13: 0x69, _14: 0x6A, _15: 0x6B, _16: 0x6C, _17: 0x6D, _18: 0x6E }; export const MP3 = { Null: 0, Back: 1, Play: 2, FF: 3, Stop: 4, Repeat_On: 5, Repeat_Off: 6, Shuffle: 7, // V3 }; const SourceIndex = [ Source._01, Source._02, Source._03, Source._04, Source._05, Source._06, Source._07, Source._08, Source._09, Source._10, Source._11, Source._12, Source._13, Source._14, Source._15, Source._16, Source._17, Source._18 ]; const PartySourceIndex = [ PartySource._01, PartySource._02, PartySource._03, PartySource._04, PartySource._05, PartySource._06, PartySource._07, PartySource._08, PartySource._09, PartySource._10, PartySource._11, PartySource._12, PartySource._13, PartySource._14, PartySource._15, PartySource._16, PartySource._17, PartySource._18 ]; export default class Protocol { header; is_volume; zone; command; data; constructor({ zone = Zone.Broadcast, command = Command.Common, data = Data.Off }) { this.header = Header; this.is_volume = command === Command.Set_Volume ? IsVolume.Yes : IsVolume.No; this.zone = zone; this.command = command; this.data = data; } get_command() { const data = !Array.isArray(this.data) ? [this.data] : this.data; const command = [ this.header, this.is_volume, this.zone, this.command, ...data ]; return Protocol.add_checksum(Buffer.from(command)); } static add_checksum(command) { const checksum = Protocol.calculate_checksum(command); let checked = Buffer.alloc(command.length + 1, command); checked.writeUInt8(checksum, checked.length - 1); return checked; } static calculate_checksum(command) { let checksum = 0; command.forEach(char => checksum += char); checksum &= 0x0FF; return checksum; } static validate_checksum(data) { const actual = data[data.length - 1]; const calculated = Protocol.add_checksum(data.subarray(0, data.length - 1))[data.length - 1]; return actual === calculated; } // Doc Note: This command suppresses returned responses from Lync. static set_echo_mode(on) { const data = on ? Data.On : Data.Off; return new Protocol({ command: Command.Echo, data }); } static get_id() { return new Protocol({ command: Command.Id }); } static get_firmware() { return new Protocol({ command: Command.Firmware }); } // Developer Note: The docs say you can set a zone on this, but setting a zone makes no difference in output // // Doc Note: // Echo Data : // 1. Echo All Zone Status. // 2. Echo All Zone Name. // 3. Echo All Source Name // 4. Echo MP3 On/Off // 5. Echo MP3 File Name and Artist Name static get_status_all() { return new Protocol({ command: Command.Get_Status_All }); } static get_status_zones() { return new Protocol({ command: Command.Get_Status_Zones }); } // Doc Note: // Echo Data : Echo All Zone Status. // Reply format reference the “Tx Format” “Zone Internal Status” static get_status_all_zones() { return new Protocol({ command: Command.Get_Status_All_Zones }); } // Developer Note: Lync12 v2 Documentation is wrong for setting party mode. They have the zone bits set, and the // zone should be zero static set_party_mode(data) { return new Protocol({ command: Command.Common, data }); } static set_party_mode_number(party_source) { party_source = Lookup.ranged_number(party_source, 1, 18); return Protocol.set_party_mode(PartySourceIndex[party_source - 1]); } // Doc Note: Echo Data : Echo Zone Name static get_zone_name(zone) { return new Protocol({ command: Command.Get_Zone_Name, zone }); } // Doc Note: Echo Data : Echo Zone Source Name static get_source_name(zone, data) { return new Protocol({ command: Command.Get_Zone_Source_Name, zone, data }); } static set_all_power(on) { const data = on ? Data.All_Power_On : Data.All_Power_Off; return new Protocol({ command: Command.Common, data }); } // Docs Note: If zone address is 0 for broadcast, this command turns all on/off and response is same as "Power All On/Off" above // - If zone address is specific, response is just cmd 0x05 for that zone (14 bytes) // - there is NO response if the value is already at the desired value static set_zone_power(zone, on) { const data = on ? Data.Zone_Power_On : Data.Zone_Power_Off; return new Protocol({ command: Command.Common, zone, data }); } // Developer Note: Doc Values are way off for these static set_volume(zone, _volume) { const volume = Lookup.valid_volume(_volume); // For volume command, level 60 is 0x00, 59 is 0xFF, and 0 is 0xC4 const data = ((volume + 0x0C4) & 0x0FF); return new Protocol({ command: Command.Set_Volume, zone, data: data }); } static set_mute(zone, on) { const data = on ? Data.Mute_On : Data.Mute_Off; return new Protocol({ command: Command.Common, zone, data }); } // Docs Note: In addition to setting the input source for a specific zone, sending an Input Source Select command to // the Lync will also result in the selected zone getting turned on. // - response is cmd 0x05 for the particular zone (14 bytes) // - there is NO response if the input is already at the desired source // - there is NO response and the physical keypad doesn't show a source name if the source number is out of range static set_source(zone, data) { return new Protocol({ command: Command.Common, zone, data: data }); } static set_source_number(zone, source) { source = Lookup.ranged_number(source, 1, 18); return Protocol.set_source(zone, SourceIndex[source - 1]); } static set_dnd(zone, on) { const data = on ? Data.DND_On : Data.DND_Off; return new Protocol({ command: Command.Common, zone, data }); } // Developer Note: Doc Values are way off for these static set_balance(zone, _balance) { const balance = Lookup.valid_balance(_balance); const data = Lookup.signed_dec_to_hex(balance); return new Protocol({ command: Command.Set_Balance, zone, data }); } // Developer Note: Doc Values are way off for these static set_treble(zone, _treble) { const treble = Lookup.valid_tone(_treble); const data = Lookup.signed_dec_to_hex(treble); return new Protocol({ command: Command.Set_Treble, zone, data }); } // Developer Note: Doc Values are way off for these static set_bass(zone, _bass) { const bass = Lookup.valid_tone(_bass); const data = Lookup.signed_dec_to_hex(bass); return new Protocol({ command: Command.Set_Bass, zone, data }); } // Developer Note: The docs say this resets a single zone name, but I'm seeing it reset all zones and sources static reset_zone_names() { return new Protocol({ command: Command.Set_Name_Default }); } static reset_zone_audio(zone) { return new Protocol({ command: Command.Set_Audio_Default, zone }); } static set_zone_name(zone, name) { if (name.length > 10) { name = name.substring(0, 10); } const data = [0x00]; for (let i = 0; i < name.length; i++) { data.push(name.charCodeAt(i)); } while (data.length < 12) { data.push(0x00); } return new Protocol({ command: Command.Set_Name_Zone, zone, data }); } // FIXME: I'm unsure as of yet if data[0] should be 1-18 (per the docs) or the source hex // Doc Note: Each Zone has its own set of Source Names that can be set. static set_zone_source_name(zone, source, name) { if (name.length > 10) { name = name.substring(0, 10); } const data = [source]; for (let i = 0; i < name.length; i++) { data.push(name.charCodeAt(i)); } while (data.length < 12) { data.push(0x00); } return new Protocol({ command: Command.Set_Name_Source, zone, data }); } static recall_file(file) { const data = Lookup.ranged_number(file, 1, 4); return new Protocol({ command: Command.Recall_File, data }); } static save_file(file) { const data = Lookup.ranged_number(file, 1, 4); return new Protocol({ command: Command.Save_File, data }); } static mp3(action) { switch (action) { case MP3.Back: return new Protocol({ command: Command.Common, data: Data.MP3_Back }); case MP3.Play: return new Protocol({ command: Command.Common, data: Data.MP3_Play }); case MP3.FF: return new Protocol({ command: Command.Common, data: Data.MP3_FF }); case MP3.Stop: return new Protocol({ command: Command.Common, data: Data.MP3_Stop }); case MP3.Repeat_On: return new Protocol({ command: Command.Set_MP3_Repeat, data: Data.On }); case MP3.Repeat_Off: return new Protocol({ command: Command.Set_MP3_Repeat, data: Data.Off }); case MP3.Shuffle: return new Protocol({ command: Command.Set_MP3_Repeat, data: Data.Shuffle }); // V3 default: throw new Error('Unrecognized MP3 Command'); } } }