@bobfrankston/lxlan 0.1.12 → 0.1.13

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/client.d.ts CHANGED
@@ -13,6 +13,8 @@ export interface LxClientOptions {
13
13
  port?: number;
14
14
  /** Auto-discovery interval in ms (0 = manual only, default 0) */
15
15
  discoveryInterval?: number;
16
+ /** Enable client-to-client state broadcast for multi-client sync (default false) */
17
+ clientBroadcastEnabled?: boolean;
16
18
  }
17
19
  /**
18
20
  * LIFX LAN client for device discovery and control.
@@ -39,6 +41,7 @@ export declare class LxClient {
39
41
  private transport;
40
42
  private port;
41
43
  private discoveryTimer?;
44
+ private clientBroadcastEnabled;
42
45
  /** Cached devices by MAC address (lowercase) */
43
46
  devices: Map<string, LxDevice>;
44
47
  constructor(options: LxClientOptions);
@@ -76,5 +79,11 @@ export declare class LxClient {
76
79
  private handleMessage;
77
80
  /** Check if device info is complete and emit deviceInfo event */
78
81
  private checkDeviceInfoComplete;
82
+ /**
83
+ * Broadcast state message to other clients on the network
84
+ * Used for multi-client state synchronization
85
+ * @param stateMessage - Raw State message bytes from device
86
+ */
87
+ private broadcastState;
79
88
  }
80
89
  //# sourceMappingURL=client.d.ts.map
package/client.js CHANGED
@@ -27,12 +27,14 @@ export class LxClient {
27
27
  transport;
28
28
  port;
29
29
  discoveryTimer; // NodeJS.Timeout in Node, number in browser
30
+ clientBroadcastEnabled;
30
31
  /** Cached devices by MAC address (lowercase) */
31
32
  devices = new Map();
32
33
  constructor(options) {
33
34
  this.emitter = options.eventEmitter;
34
35
  this.transport = options.transport;
35
36
  this.port = options.port ?? LIFX_PORT;
37
+ this.clientBroadcastEnabled = options.clientBroadcastEnabled ?? false;
36
38
  this.transport.onMessage((data, rinfo) => {
37
39
  // Ensure data is Uint8Array for protocol decoder
38
40
  const buf = data instanceof Uint8Array ? data : new Uint8Array(data);
@@ -183,6 +185,12 @@ export class LxClient {
183
185
  device.markResponseReceived(MessageType.SetPower);
184
186
  device.markResponseReceived(MessageType.SetColor);
185
187
  this.emit('state', device);
188
+ // Multi-client state synchronization: broadcast to other clients
189
+ // Safe fail approach: only rebroadcast if from device IP
190
+ // If not from device.ip, assume it's a client broadcast - just update local state, don't rebroadcast
191
+ if (this.clientBroadcastEnabled && fromIp === device.ip) {
192
+ this.broadcastState(data);
193
+ }
186
194
  break;
187
195
  }
188
196
  case MessageType.StatePower: {
@@ -262,5 +270,21 @@ export class LxClient {
262
270
  this.emit('deviceInfo', device);
263
271
  }
264
272
  }
273
+ /**
274
+ * Broadcast state message to other clients on the network
275
+ * Used for multi-client state synchronization
276
+ * @param stateMessage - Raw State message bytes from device
277
+ */
278
+ broadcastState(stateMessage) {
279
+ try {
280
+ // Broadcast the State message so other clients can update their local state
281
+ // Using broadcast address on standard LIFX port (56700)
282
+ this.transport.send('255.255.255.255', this.port, stateMessage);
283
+ }
284
+ catch (err) {
285
+ // Don't emit error for broadcast failures - it's non-critical
286
+ console.warn('Failed to broadcast state to other clients:', err);
287
+ }
288
+ }
265
289
  }
266
290
  //# sourceMappingURL=client.js.map
package/device.js CHANGED
@@ -125,7 +125,8 @@ export class LxDevice {
125
125
  type: MessageType.SetPower,
126
126
  target: this.mac,
127
127
  payload: encodeSetPower(on),
128
- ackRequired: true
128
+ ackRequired: true,
129
+ resRequired: true // Get State response with full state
129
130
  });
130
131
  this.transport.send(this.ip, this.port, msg);
131
132
  // Track this request with a timeout
@@ -155,7 +156,8 @@ export class LxDevice {
155
156
  type: MessageType.SetColor,
156
157
  target: this.mac,
157
158
  payload: encodeSetColor(hsbk16, duration),
158
- ackRequired: true
159
+ ackRequired: true,
160
+ resRequired: true // Get State response with full state
159
161
  });
160
162
  this.transport.send(this.ip, this.port, msg);
161
163
  // Track this request with a timeout
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/lxlan",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "LIFX LAN protocol library for device control via UDP",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/protocol.d.ts CHANGED
@@ -1,5 +1,16 @@
1
1
  import { HSBK16 } from '@bobfrankston/colorlib';
2
2
  import { LxMessage } from './types.js';
3
+ /** LIFX protocol frame flags */
4
+ export declare const FrameFlags: {
5
+ readonly ResRequired: 1;
6
+ readonly AckRequired: 2; /** Acknowledgement required from device */
7
+ };
8
+ /** LIFX protocol bits */
9
+ export declare const ProtocolBits: {
10
+ readonly Tagged: 8192;
11
+ readonly Addressable: 4096;
12
+ readonly ProtocolMask: 4095; /** Protocol version mask */
13
+ };
3
14
  /** Get next sequence number (0-255 wrapping) */
4
15
  export declare function nextSequence(): number;
5
16
  /** Get global source ID */
package/protocol.js CHANGED
@@ -1,4 +1,15 @@
1
1
  import { HEADER_SIZE } from './types.js';
2
+ /** LIFX protocol frame flags */
3
+ export const FrameFlags = {
4
+ ResRequired: 0x01, /** Response required from device */
5
+ AckRequired: 0x02 /** Acknowledgement required from device */
6
+ };
7
+ /** LIFX protocol bits */
8
+ export const ProtocolBits = {
9
+ Tagged: 0x2000, /** Message is tagged (broadcast) */
10
+ Addressable: 0x1000, /** Message is addressable */
11
+ ProtocolMask: 0xFFF /** Protocol version mask */
12
+ };
2
13
  let globalSource = Math.floor(Math.random() * 0xFFFFFFFF);
3
14
  let sequenceCounter = 0;
4
15
  /** Get next sequence number (0-255 wrapping) */
@@ -80,12 +91,12 @@ export function encodeMessage(options) {
80
91
  const sequence = options.sequence ?? nextSequence();
81
92
  // Frame (8 bytes)
82
93
  writeUInt16LE(buf, size, 0);
83
- const protocol = 1024 | (tagged ? 0x2000 : 0) | 0x1000;
94
+ const protocol = 1024 | (tagged ? ProtocolBits.Tagged : 0) | ProtocolBits.Addressable;
84
95
  writeUInt16LE(buf, protocol, 2);
85
96
  writeUInt32LE(buf, globalSource, 4);
86
97
  // Frame Address (16 bytes)
87
98
  buf.set(macToBuffer(target), 8);
88
- const flags = (options.resRequired ? 0x01 : 0) | (options.ackRequired ? 0x02 : 0);
99
+ const flags = (options.resRequired ? FrameFlags.ResRequired : 0) | (options.ackRequired ? FrameFlags.AckRequired : 0);
89
100
  buf[22] = flags;
90
101
  buf[23] = sequence;
91
102
  // Protocol Header (12 bytes)
@@ -103,9 +114,9 @@ export function decodeMessage(buf) {
103
114
  }
104
115
  const size = readUInt16LE(buf, 0);
105
116
  const protocolBits = readUInt16LE(buf, 2);
106
- const tagged = (protocolBits & 0x2000) !== 0;
107
- const addressable = (protocolBits & 0x1000) !== 0;
108
- const protocol = protocolBits & 0xFFF;
117
+ const tagged = (protocolBits & ProtocolBits.Tagged) !== 0;
118
+ const addressable = (protocolBits & ProtocolBits.Addressable) !== 0;
119
+ const protocol = protocolBits & ProtocolBits.ProtocolMask;
109
120
  const source = readUInt32LE(buf, 4);
110
121
  const target = bufferToMac(buf, 8);
111
122
  const flags = buf[22];
@@ -119,8 +130,8 @@ export function decodeMessage(buf) {
119
130
  tagged,
120
131
  source,
121
132
  target,
122
- resRequired: (flags & 0x01) !== 0,
123
- ackRequired: (flags & 0x02) !== 0,
133
+ resRequired: (flags & FrameFlags.ResRequired) !== 0,
134
+ ackRequired: (flags & FrameFlags.AckRequired) !== 0,
124
135
  sequence,
125
136
  type,
126
137
  payload