@bobfrankston/lxlan 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/protocol.ts DELETED
@@ -1,209 +0,0 @@
1
- import { HSBK16 } from '@bobfrankston/colorlib';
2
- import { LxMessage, LxHeader, HEADER_SIZE, MessageType } from './types.js';
3
-
4
- let globalSource = Math.floor(Math.random() * 0xFFFFFFFF);
5
- let sequenceCounter = 0;
6
-
7
- /** Get next sequence number (0-255 wrapping) */
8
- export function nextSequence(): number {
9
- sequenceCounter = (sequenceCounter + 1) & 0xFF;
10
- return sequenceCounter;
11
- }
12
-
13
- /** Get global source ID */
14
- export function getSource(): number {
15
- return globalSource;
16
- }
17
-
18
- /** MAC string to 8-byte buffer (6 bytes MAC + 2 padding) */
19
- function macToBuffer(mac: string): Buffer {
20
- const buf = Buffer.alloc(8);
21
- if (mac && mac !== '00:00:00:00:00:00') {
22
- const bytes = mac.split(':').map(b => parseInt(b, 16));
23
- for (let i = 0; i < 6; i++) buf[i] = bytes[i] || 0;
24
- }
25
- return buf;
26
- }
27
-
28
- /** Buffer to MAC string */
29
- function bufferToMac(buf: Buffer, offset: number = 0): string {
30
- const parts: string[] = [];
31
- for (let i = 0; i < 6; i++) {
32
- parts.push(buf[offset + i].toString(16).padStart(2, '0'));
33
- }
34
- return parts.join(':');
35
- }
36
-
37
- /** Encode LIFX message to buffer */
38
- export function encodeMessage(options: {
39
- type: number;
40
- target?: string; /** MAC address, empty for broadcast */
41
- payload?: Buffer;
42
- tagged?: boolean;
43
- ackRequired?: boolean;
44
- resRequired?: boolean;
45
- sequence?: number;
46
- }): Buffer {
47
- const payload = options.payload ?? Buffer.alloc(0);
48
- const size = HEADER_SIZE + payload.length;
49
- const buf = Buffer.alloc(size);
50
-
51
- const tagged = options.tagged ?? !options.target;
52
- const target = options.target ?? '00:00:00:00:00:00';
53
- const sequence = options.sequence ?? nextSequence();
54
-
55
- // Frame (8 bytes)
56
- buf.writeUInt16LE(size, 0); // size
57
- const protocol = 1024 | (tagged ? 0x2000 : 0) | 0x1000; // addressable=1, protocol=1024
58
- buf.writeUInt16LE(protocol, 2);
59
- buf.writeUInt32LE(globalSource, 4); // source
60
-
61
- // Frame Address (16 bytes)
62
- macToBuffer(target).copy(buf, 8); // target (8 bytes)
63
- // reserved (6 bytes at offset 16)
64
- const flags = (options.resRequired ? 0x01 : 0) | (options.ackRequired ? 0x02 : 0);
65
- buf[22] = flags;
66
- buf[23] = sequence;
67
-
68
- // Protocol Header (12 bytes)
69
- // reserved (8 bytes at offset 24)
70
- buf.writeUInt16LE(options.type, 32); // type
71
- // reserved (2 bytes at offset 34)
72
-
73
- // Payload
74
- if (payload.length > 0) {
75
- payload.copy(buf, HEADER_SIZE);
76
- }
77
-
78
- return buf;
79
- }
80
-
81
- /** Decode LIFX message from buffer */
82
- export function decodeMessage(buf: Buffer): LxMessage {
83
- if (buf.length < HEADER_SIZE) {
84
- throw new Error(`Buffer too small: ${buf.length} < ${HEADER_SIZE}`);
85
- }
86
-
87
- const size = buf.readUInt16LE(0);
88
- const protocolBits = buf.readUInt16LE(2);
89
- const tagged = (protocolBits & 0x2000) !== 0;
90
- const addressable = (protocolBits & 0x1000) !== 0;
91
- const protocol = protocolBits & 0xFFF;
92
-
93
- const source = buf.readUInt32LE(4);
94
- const target = bufferToMac(buf, 8);
95
- const flags = buf[22];
96
- const sequence = buf[23];
97
- const type = buf.readUInt16LE(32);
98
-
99
- const payload = buf.length > HEADER_SIZE ? buf.subarray(HEADER_SIZE) : Buffer.alloc(0);
100
-
101
- return {
102
- size,
103
- protocol,
104
- addressable,
105
- tagged,
106
- source,
107
- target,
108
- resRequired: (flags & 0x01) !== 0,
109
- ackRequired: (flags & 0x02) !== 0,
110
- sequence,
111
- type,
112
- payload
113
- };
114
- }
115
-
116
- /** Encode SetPower payload */
117
- export function encodeSetPower(on: boolean): Buffer {
118
- const buf = Buffer.alloc(2);
119
- buf.writeUInt16LE(on ? 0xFFFF : 0, 0);
120
- return buf;
121
- }
122
-
123
- /** Encode SetColor payload */
124
- export function encodeSetColor(hsbk: HSBK16, duration: number = 0): Buffer {
125
- const buf = Buffer.alloc(13);
126
- buf[0] = 0; // reserved
127
- buf.writeUInt16LE(hsbk.h, 1);
128
- buf.writeUInt16LE(hsbk.s, 3);
129
- buf.writeUInt16LE(hsbk.b, 5);
130
- buf.writeUInt16LE(hsbk.k, 7);
131
- buf.writeUInt32LE(duration, 9);
132
- return buf;
133
- }
134
-
135
- /** Encode SetLabel payload */
136
- export function encodeSetLabel(label: string): Buffer {
137
- const buf = Buffer.alloc(32);
138
- buf.write(label.substring(0, 32), 0, 'utf8');
139
- return buf;
140
- }
141
-
142
- /** Encode SetGroup payload */
143
- export function encodeSetGroup(id: string, label: string): Buffer {
144
- const buf = Buffer.alloc(56);
145
- // 16-byte group ID
146
- const idBytes = Buffer.from(id.replace(/-/g, ''), 'hex');
147
- idBytes.copy(buf, 0, 0, Math.min(16, idBytes.length));
148
- // 32-byte label
149
- buf.write(label.substring(0, 32), 16, 'utf8');
150
- // 8-byte updated_at timestamp (ns since epoch)
151
- const now = BigInt(Date.now()) * BigInt(1000000);
152
- buf.writeBigUInt64LE(now, 48);
153
- return buf;
154
- }
155
-
156
- /** Encode SetLocation payload (same format as SetGroup) */
157
- export function encodeSetLocation(id: string, label: string): Buffer {
158
- return encodeSetGroup(id, label);
159
- }
160
-
161
- /** Decode State (107) payload - light state */
162
- export function decodeState(payload: Buffer): { hsbk: HSBK16; power: number; label: string } {
163
- return {
164
- hsbk: {
165
- h: payload.readUInt16LE(0),
166
- s: payload.readUInt16LE(2),
167
- b: payload.readUInt16LE(4),
168
- k: payload.readUInt16LE(6)
169
- },
170
- power: payload.readUInt16LE(10),
171
- label: payload.toString('utf8', 12, 44).replace(/\0+$/, '')
172
- };
173
- }
174
-
175
- /** Decode StatePower (22) payload */
176
- export function decodeStatePower(payload: Buffer): boolean {
177
- return payload.readUInt16LE(0) > 0;
178
- }
179
-
180
- /** Decode StateLabel (25) payload */
181
- export function decodeStateLabel(payload: Buffer): string {
182
- return payload.toString('utf8', 0, 32).replace(/\0+$/, '');
183
- }
184
-
185
- /** Decode StateVersion (33) payload */
186
- export function decodeStateVersion(payload: Buffer): { vendor: number; product: number; version: number } {
187
- return {
188
- vendor: payload.readUInt32LE(0),
189
- product: payload.readUInt32LE(4),
190
- version: payload.readUInt32LE(8)
191
- };
192
- }
193
-
194
- /** Decode StateGroup (53) or StateLocation (50) payload */
195
- export function decodeStateGroup(payload: Buffer): { id: string; label: string; updatedAt: number } {
196
- const idBytes = payload.subarray(0, 16);
197
- const id = idBytes.toString('hex');
198
- const label = payload.toString('utf8', 16, 48).replace(/\0+$/, '');
199
- const updatedAt = Number(payload.readBigUInt64LE(48) / BigInt(1000000));
200
- return { id, label, updatedAt };
201
- }
202
-
203
- /** Decode StateService (3) payload */
204
- export function decodeStateService(payload: Buffer): { service: number; port: number } {
205
- return {
206
- service: payload[0],
207
- port: payload.readUInt32LE(1)
208
- };
209
- }
package/tsconfig.json DELETED
@@ -1,20 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "esnext",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "allowSyntheticDefaultImports": true,
7
- "esModuleInterop": true,
8
- "strict": true,
9
- "forceConsistentCasingInFileNames": true,
10
- "skipLibCheck": true,
11
- "declaration": true,
12
- "declarationMap": true,
13
- "sourceMap": true,
14
- "strictNullChecks": false,
15
- "noImplicitAny": true,
16
- "noImplicitThis": true,
17
- "newLine": "lf"
18
- },
19
- "exclude": ["node_modules", "cruft", ".git", "tests", "prev"]
20
- }
package/types.ts DELETED
@@ -1,129 +0,0 @@
1
- import { HSBK } from '@bobfrankston/colorlib';
2
-
3
- /** LIFX message types */
4
- export const MessageType = {
5
- GetService: 2,
6
- StateService: 3,
7
- GetPower: 20,
8
- SetPower: 21,
9
- StatePower: 22,
10
- GetLabel: 23,
11
- SetLabel: 24,
12
- StateLabel: 25,
13
- GetVersion: 32,
14
- StateVersion: 33,
15
- GetLocation: 48,
16
- SetLocation: 49,
17
- StateLocation: 50,
18
- GetGroup: 51,
19
- SetGroup: 52,
20
- StateGroup: 53,
21
- Get: 101, /** GetColor */
22
- SetColor: 102,
23
- State: 107, /** LightState */
24
- } as const;
25
-
26
- export type MessageTypeValue = typeof MessageType[keyof typeof MessageType];
27
-
28
- /** LIFX protocol header */
29
- export interface LxHeader {
30
- size: number;
31
- protocol: number;
32
- addressable: boolean;
33
- tagged: boolean;
34
- source: number;
35
- target: string; /** MAC address as hex string */
36
- resRequired: boolean;
37
- ackRequired: boolean;
38
- sequence: number;
39
- type: number;
40
- }
41
-
42
- /** Full LIFX message */
43
- export interface LxMessage extends LxHeader {
44
- payload: Buffer;
45
- }
46
-
47
- /** Group/Location info */
48
- export interface GroupInfo {
49
- id: string; /** 16-byte GUID as hex */
50
- label: string;
51
- updatedAt: number; /** timestamp */
52
- }
53
-
54
- /** Device state for caching */
55
- export interface DeviceState {
56
- power: boolean;
57
- color: HSBK;
58
- label: string;
59
- group: GroupInfo;
60
- location: GroupInfo;
61
- vendor: number;
62
- product: number;
63
- }
64
-
65
- /** LIFX port */
66
- export const LIFX_PORT = 56700;
67
-
68
- /** Header size */
69
- export const HEADER_SIZE = 36;
70
-
71
- /** Product info - common LIFX products */
72
- export const Products: Record<number, string> = {
73
- 1: 'LIFX Original 1000',
74
- 3: 'LIFX Color 650',
75
- 10: 'LIFX White 800 (Low Voltage)',
76
- 11: 'LIFX White 800 (High Voltage)',
77
- 18: 'LIFX White 900 BR30 (Low Voltage)',
78
- 20: 'LIFX Color 1000 BR30',
79
- 22: 'LIFX Color 1000',
80
- 27: 'LIFX A19',
81
- 28: 'LIFX BR30',
82
- 29: 'LIFX A19 Night Vision',
83
- 30: 'LIFX BR30 Night Vision',
84
- 31: 'LIFX Z',
85
- 32: 'LIFX Z 2',
86
- 36: 'LIFX Downlight',
87
- 37: 'LIFX Downlight',
88
- 38: 'LIFX Beam',
89
- 43: 'LIFX A19',
90
- 44: 'LIFX BR30',
91
- 45: 'LIFX A19 Night Vision',
92
- 46: 'LIFX BR30 Night Vision',
93
- 49: 'LIFX Mini',
94
- 50: 'LIFX Mini Day and Dusk',
95
- 51: 'LIFX Mini White',
96
- 52: 'LIFX GU10',
97
- 55: 'LIFX Tile',
98
- 57: 'LIFX Candle',
99
- 59: 'LIFX Mini Color',
100
- 60: 'LIFX Mini Day and Dusk',
101
- 61: 'LIFX Mini White',
102
- 62: 'LIFX A19',
103
- 63: 'LIFX BR30',
104
- 64: 'LIFX A19 Night Vision',
105
- 65: 'LIFX BR30 Night Vision',
106
- 66: 'LIFX Mini Color',
107
- 68: 'LIFX Candle',
108
- 81: 'LIFX Candle White to Warm',
109
- 82: 'LIFX Filament Clear',
110
- 85: 'LIFX Filament Amber',
111
- 87: 'LIFX Mini White',
112
- 88: 'LIFX Mini White',
113
- 89: 'LIFX Switch',
114
- 90: 'LIFX Clean',
115
- 91: 'LIFX Color',
116
- 92: 'LIFX Color',
117
- 93: 'LIFX A19 US',
118
- 94: 'LIFX BR30',
119
- 96: 'LIFX Candle White to Warm',
120
- 97: 'LIFX A19',
121
- 98: 'LIFX BR30',
122
- 99: 'LIFX Clean',
123
- 100: 'LIFX Filament Clear',
124
- 101: 'LIFX Filament Amber',
125
- 109: 'LIFX A19 Night Vision',
126
- 110: 'LIFX BR30 Night Vision',
127
- 111: 'LIFX A19 Night Vision Intl',
128
- 112: 'LIFX BR30 Night Vision Intl',
129
- };