@bobfrankston/lxlan 0.1.13 → 0.1.15

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/.hintrc CHANGED
@@ -28,6 +28,7 @@
28
28
  {
29
29
  "label": "off"
30
30
  }
31
- ]
31
+ ],
32
+ "typescript-config/strict": "off"
32
33
  }
33
34
  }
package/README.md CHANGED
@@ -127,8 +127,13 @@ client.on('device', (device) => {
127
127
  // New device discovered
128
128
  });
129
129
 
130
- client.on('state', (device) => {
130
+ client.on('state', (device, info) => {
131
131
  // Device state updated
132
+ // info.primary = true if direct response from device
133
+ // info.primary = false if received via peer client broadcast
134
+ if (info.primary) {
135
+ console.log('Direct update from device');
136
+ }
132
137
  });
133
138
 
134
139
  client.on('deviceInfo', (device) => {
@@ -141,10 +146,22 @@ client.on('deviceInfo', (device) => {
141
146
 
142
147
  - `@bobfrankston/colorlib` - Color space conversions (HSB ↔ RGB ↔ Kelvin)
143
148
 
149
+ ## Keeping Transport Packages in Sync
150
+
151
+ **IMPORTANT:** `lxlan-browser` and `lxlan-node` are parallel transport adapters. When modifying types or exports in this core package, ensure both adapters are updated if needed. They should expose equivalent functionality for their respective platforms.
152
+
153
+ ## Importing Types
154
+
155
+ Types should be imported from this package using `import type`:
156
+
157
+ ```typescript
158
+ import type { StateEventInfo, DeviceState } from '@bobfrankston/lxlan';
159
+ ```
160
+
144
161
  ## Related Packages
145
162
 
146
- - **[@bobfrankston/lxlan-node](../lxlan-node)** - Node.js transport wrapper
147
- - **[@bobfrankston/lxlan-browser](../lxlan-browser)** - Browser transport wrapper
163
+ - **[@bobfrankston/lxlan-node](../lxlan-node)** - Node.js transport wrapper (keep in sync!)
164
+ - **[@bobfrankston/lxlan-browser](../lxlan-browser)** - Browser transport wrapper (keep in sync!)
148
165
  - **[@bobfrankston/rmfudp](../../../../utils/udp/rmfudp)** - Node.js UDP transport
149
166
  - **[@bobfrankston/httpudp-client](../../../../utils/udp/httpudp-client)** - Browser WebSocket UDP client
150
167
  - **[@bobfrankston/httpudp](../../../../utils/udp/httpudp)** - WebSocket-to-UDP proxy server
package/client.d.ts CHANGED
@@ -15,6 +15,8 @@ export interface LxClientOptions {
15
15
  discoveryInterval?: number;
16
16
  /** Enable client-to-client state broadcast for multi-client sync (default false) */
17
17
  clientBroadcastEnabled?: boolean;
18
+ /** Custom logger function (default: console.log) */
19
+ logger?: (msg: string, colors?: string) => void;
18
20
  }
19
21
  /**
20
22
  * LIFX LAN client for device discovery and control.
@@ -23,7 +25,7 @@ export interface LxClientOptions {
23
25
  * Events:
24
26
  * - `device` (device: LxDevice) - new device discovered
25
27
  * - `message` (device: LxDevice, msg: LxMessage) - any message received
26
- * - `state` (device: LxDevice) - light state updated
28
+ * - `state` (device: LxDevice, info: StateEventInfo) - light state updated (info.primary = true if from device, false if from peer broadcast)
27
29
  * - `power` (device: LxDevice) - power state updated
28
30
  * - `label` (device: LxDevice) - label updated
29
31
  * - `group` (device: LxDevice) - group info updated
@@ -41,7 +43,9 @@ export declare class LxClient {
41
43
  private transport;
42
44
  private port;
43
45
  private discoveryTimer?;
46
+ private discoveryIntervalMs;
44
47
  private clientBroadcastEnabled;
48
+ private log;
45
49
  /** Cached devices by MAC address (lowercase) */
46
50
  devices: Map<string, LxDevice>;
47
51
  constructor(options: LxClientOptions);
@@ -53,7 +57,7 @@ export declare class LxClient {
53
57
  off(event: string, listener: (...args: any[]) => void): this;
54
58
  /** Emit event */
55
59
  private emit;
56
- /** Start listening */
60
+ /** Start listening and begin auto-discovery if configured */
57
61
  start(): Promise<void>;
58
62
  /** Stop and cleanup */
59
63
  stop(): void;
package/client.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { hsbk16ToHsbk } from '@bobfrankston/colorlib';
2
2
  import { LxDevice } from './device.js';
3
- import { MessageType, LIFX_PORT } from './types.js';
3
+ import { MessageType, LIFX_PORT, MessageTypeName } from './types.js';
4
4
  import { encodeMessage, decodeMessage, decodeState, decodeStatePower, decodeStateLabel, decodeStateVersion, decodeStateGroup, decodeStateService, decodeStateHostInfo, decodeStateHostFirmware, decodeStateWifiInfo, decodeStateInfo } from './protocol.js';
5
5
  /**
6
6
  * LIFX LAN client for device discovery and control.
@@ -9,7 +9,7 @@ import { encodeMessage, decodeMessage, decodeState, decodeStatePower, decodeStat
9
9
  * Events:
10
10
  * - `device` (device: LxDevice) - new device discovered
11
11
  * - `message` (device: LxDevice, msg: LxMessage) - any message received
12
- * - `state` (device: LxDevice) - light state updated
12
+ * - `state` (device: LxDevice, info: StateEventInfo) - light state updated (info.primary = true if from device, false if from peer broadcast)
13
13
  * - `power` (device: LxDevice) - power state updated
14
14
  * - `label` (device: LxDevice) - label updated
15
15
  * - `group` (device: LxDevice) - group info updated
@@ -27,7 +27,9 @@ export class LxClient {
27
27
  transport;
28
28
  port;
29
29
  discoveryTimer; // NodeJS.Timeout in Node, number in browser
30
+ discoveryIntervalMs;
30
31
  clientBroadcastEnabled;
32
+ log;
31
33
  /** Cached devices by MAC address (lowercase) */
32
34
  devices = new Map();
33
35
  constructor(options) {
@@ -35,6 +37,7 @@ export class LxClient {
35
37
  this.transport = options.transport;
36
38
  this.port = options.port ?? LIFX_PORT;
37
39
  this.clientBroadcastEnabled = options.clientBroadcastEnabled ?? false;
40
+ this.log = options.logger ?? ((msg, colors) => console.log(msg));
38
41
  this.transport.onMessage((data, rinfo) => {
39
42
  // Ensure data is Uint8Array for protocol decoder
40
43
  const buf = data instanceof Uint8Array ? data : new Uint8Array(data);
@@ -43,19 +46,7 @@ export class LxClient {
43
46
  this.transport.onError((err) => {
44
47
  this.emitter.emit('error', err);
45
48
  });
46
- if (options.discoveryInterval && options.discoveryInterval > 0) {
47
- this.discoveryTimer = setInterval(() => {
48
- try {
49
- this.discover();
50
- }
51
- catch (err) {
52
- // Emit error but keep timer running
53
- if (err instanceof Error) {
54
- this.emitter.emit('error', err);
55
- }
56
- }
57
- }, options.discoveryInterval);
58
- }
49
+ this.discoveryIntervalMs = options.discoveryInterval ?? 0;
59
50
  }
60
51
  /** Register event listener */
61
52
  on(event, listener) {
@@ -76,9 +67,19 @@ export class LxClient {
76
67
  emit(event, ...args) {
77
68
  return this.emitter.emit(event, ...args);
78
69
  }
79
- /** Start listening */
70
+ /** Start listening and begin auto-discovery if configured */
80
71
  async start() {
81
72
  await this.transport.bind();
73
+ if (this.discoveryIntervalMs > 0 && !this.discoveryTimer) {
74
+ this.discoveryTimer = setInterval(() => {
75
+ try {
76
+ this.discover();
77
+ }
78
+ catch (err) {
79
+ this.emitter.emit('error', err instanceof Error ? err : new Error(String(err)));
80
+ }
81
+ }, this.discoveryIntervalMs);
82
+ }
82
83
  }
83
84
  /** Stop and cleanup */
84
85
  stop() {
@@ -138,19 +139,25 @@ export class LxClient {
138
139
  }
139
140
  /** Handle incoming message */
140
141
  handleMessage(data, fromIp, fromPort) {
142
+ this.log(`[LxClient] handleMessage: ${data.length} bytes from ${fromIp}:${fromPort}`);
141
143
  let msg;
142
144
  try {
143
145
  msg = decodeMessage(data);
146
+ const typeName = MessageTypeName(msg.type);
147
+ this.log(`[LxClient] decoded message type=${msg.type} (${typeName}), target=${msg.target}`);
144
148
  }
145
149
  catch (e) {
146
150
  this.emit('error', new Error(`Decode error: ${e.message}`));
147
151
  return;
148
152
  }
149
153
  const mac = msg.target;
150
- if (mac === '00:00:00:00:00:00')
151
- return; /** broadcast response without target */
154
+ if (mac === '00:00:00:00:00:00') {
155
+ this.log(`[LxClient] ignoring broadcast response (no target)`);
156
+ return;
157
+ }
152
158
  let device = this.devices.get(mac);
153
159
  const isNew = !device;
160
+ this.log(`[LxClient] mac=${mac}, isNew=${isNew}, deviceCount=${this.devices.size}`);
154
161
  if (!device) {
155
162
  device = new LxDevice(mac, fromIp);
156
163
  device.setTransport(this.transport);
@@ -161,12 +168,16 @@ export class LxClient {
161
168
  device.port = fromPort;
162
169
  device.markSeen();
163
170
  // Check for duplicate message (some LIFX devices send responses twice)
164
- if (device.isDuplicate(msg.sequence)) {
165
- return; // Ignore duplicate
171
+ // Use both sequence AND type - different types with same seq are valid (e.g., Ack + State)
172
+ if (device.isDuplicate(msg.sequence, msg.type)) {
173
+ this.log(`[LxClient] ignoring duplicate seq=${msg.sequence} type=${msg.type}`);
174
+ return;
166
175
  }
167
176
  if (isNew) {
168
177
  this.emit('device', device);
169
178
  }
179
+ const typeName = MessageTypeName(msg.type);
180
+ this.log(`[LxClient] processing msg.type=#${msg.type} ${typeName} (State=${MessageType.State})`);
170
181
  this.emit('message', device, msg);
171
182
  switch (msg.type) {
172
183
  case MessageType.StateService: {
@@ -176,7 +187,12 @@ export class LxClient {
176
187
  break;
177
188
  }
178
189
  case MessageType.State: {
190
+ // Hex dump first 20 bytes to debug power offset
191
+ const hex = Array.from(msg.payload.slice(0, 20)).map(b => b.toString(16).padStart(2, '0')).join(' ');
192
+ this.log(`[LxClient] State payload[0:20]: ${hex}`, 'navy,white');
179
193
  const state = decodeState(msg.payload);
194
+ this.log(`[LxClient] raw state.power=${state.power} (0=off, 65535=on) at offset 10`);
195
+ this.log(`[LxClient] raw state.hsbk=${JSON.stringify(state)} at offset 12`);
180
196
  device.color = hsbk16ToHsbk(state.hsbk);
181
197
  device.power = state.power > 0;
182
198
  device.label = state.label;
@@ -184,11 +200,14 @@ export class LxClient {
184
200
  // State messages also respond to SetPower and SetColor
185
201
  device.markResponseReceived(MessageType.SetPower);
186
202
  device.markResponseReceived(MessageType.SetColor);
187
- this.emit('state', device);
203
+ // primary = true if from device, false if from peer client broadcast
204
+ const primary = fromIp === device.ip;
205
+ this.log(`[LxClient] emitting 'state' for ${device.mac}, power=${device.power}, primary=${primary}`);
206
+ this.emit('state', device, { primary });
188
207
  // Multi-client state synchronization: broadcast to other clients
189
208
  // Safe fail approach: only rebroadcast if from device IP
190
209
  // 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) {
210
+ if (this.clientBroadcastEnabled && primary) {
192
211
  this.broadcastState(data);
193
212
  }
194
213
  break;
package/device.d.ts CHANGED
@@ -44,8 +44,8 @@ export declare class LxDevice {
44
44
  group: GroupInfo;
45
45
  /** Location membership */
46
46
  location: GroupInfo;
47
- /** Recent message sequences for deduplication (sequence -> timestamp) */
48
- private recentSequences;
47
+ /** Recent messages for deduplication (sequence:type -> timestamp) */
48
+ private recentMessages;
49
49
  private transport?;
50
50
  /** Pending requests awaiting responses (message type -> timeout) */
51
51
  private pendingRequests;
@@ -80,11 +80,24 @@ export declare class LxDevice {
80
80
  /** Set power on/off */
81
81
  setPower(on: boolean): void;
82
82
  /**
83
- * Set brightness while keeping current color
83
+ * Set brightness without affecting hue, saturation, or kelvin.
84
+ * Uses SetWaveformOptional (type 119) — device keeps its current color temperature.
84
85
  * @param brightness - Brightness 0-100
85
86
  * @param duration - Transition time in milliseconds (default 0)
86
87
  */
87
88
  setBrightness(brightness: number, duration?: number): void;
89
+ /**
90
+ * Set individual HSBK values without affecting unspecified fields.
91
+ * Uses SetWaveformOptional (type 119) — only fields present in the object are applied.
92
+ * @param values - Partial HSBK: { h?, s?, b?, k? } (h=0-360, s=0-100, b=0-100, k=1500-9000)
93
+ * @param duration - Transition time in milliseconds (default 0)
94
+ */
95
+ setValues(values: {
96
+ h?: number;
97
+ s?: number;
98
+ b?: number;
99
+ k?: number;
100
+ }, duration?: number): void;
88
101
  /**
89
102
  * Set color - accepts flexible input formats
90
103
  * @param color - Color as hex "#ff0000", RGB {r,g,b}, HSL {h,s,l}, HSB {h,s,b}, HSBK {h,s,b,k}, or Kelvin number
@@ -135,11 +148,13 @@ export declare class LxDevice {
135
148
  /** Notify that a response was received for a message type */
136
149
  markResponseReceived(messageType: number): void;
137
150
  /**
138
- * Check if message is a duplicate based on sequence number.
151
+ * Check if message is a duplicate based on sequence number AND type.
139
152
  * LIFX devices sometimes send duplicate responses.
153
+ * Different message types with same sequence are NOT duplicates (e.g., Ack + State).
140
154
  * @param sequence - Message sequence number
155
+ * @param type - Message type number
141
156
  * @returns true if duplicate (already seen recently)
142
157
  */
143
- isDuplicate(sequence: number): boolean;
158
+ isDuplicate(sequence: number, type: number): boolean;
144
159
  }
145
160
  //# sourceMappingURL=device.d.ts.map
package/device.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { parseColor, hsbkTo16 } from '@bobfrankston/colorlib';
2
2
  import { MessageType, Products, LIFX_PORT } from './types.js';
3
- import { encodeMessage, encodeSetPower, encodeSetColor, encodeSetLabel, encodeSetGroup, encodeSetLocation } from './protocol.js';
3
+ import { encodeMessage, encodeSetPower, encodeSetColor, encodeSetWaveformOptional, encodeSetLabel, encodeSetGroup, encodeSetLocation } from './protocol.js';
4
4
  /**
5
5
  * Represents a LIFX device on the LAN.
6
6
  * State is cached and updated via events from LxClient.
@@ -44,8 +44,8 @@ export class LxDevice {
44
44
  group = { id: '', label: '', updatedAt: 0 };
45
45
  /** Location membership */
46
46
  location = { id: '', label: '', updatedAt: 0 };
47
- /** Recent message sequences for deduplication (sequence -> timestamp) */
48
- recentSequences = new Map();
47
+ /** Recent messages for deduplication (sequence:type -> timestamp) */
48
+ recentMessages = new Map();
49
49
  transport;
50
50
  /** Pending requests awaiting responses (message type -> timeout) */
51
51
  pendingRequests = new Map();
@@ -121,6 +121,7 @@ export class LxDevice {
121
121
  /** Set power on/off */
122
122
  setPower(on) {
123
123
  this.requireTransport();
124
+ this.power = on;
124
125
  const msg = encodeMessage({
125
126
  type: MessageType.SetPower,
126
127
  target: this.mac,
@@ -133,15 +134,42 @@ export class LxDevice {
133
134
  this.startRequestTimeout(MessageType.SetPower);
134
135
  }
135
136
  /**
136
- * Set brightness while keeping current color
137
+ * Set brightness without affecting hue, saturation, or kelvin.
138
+ * Uses SetWaveformOptional (type 119) — device keeps its current color temperature.
137
139
  * @param brightness - Brightness 0-100
138
140
  * @param duration - Transition time in milliseconds (default 0)
139
141
  */
140
142
  setBrightness(brightness, duration = 0) {
143
+ this.setValues({ b: brightness }, duration);
144
+ }
145
+ /**
146
+ * Set individual HSBK values without affecting unspecified fields.
147
+ * Uses SetWaveformOptional (type 119) — only fields present in the object are applied.
148
+ * @param values - Partial HSBK: { h?, s?, b?, k? } (h=0-360, s=0-100, b=0-100, k=1500-9000)
149
+ * @param duration - Transition time in milliseconds (default 0)
150
+ */
151
+ setValues(values, duration = 0) {
141
152
  this.requireTransport();
142
- // Keep current hue/saturation/kelvin, just change brightness
143
- const newColor = { ...this.color, b: brightness };
144
- this.setColor(newColor, duration);
153
+ const hsbk16 = hsbkTo16({
154
+ h: values.h ?? 0,
155
+ s: values.s ?? 0,
156
+ b: values.b ?? 0,
157
+ k: values.k ?? 3500
158
+ });
159
+ const msg = encodeMessage({
160
+ type: MessageType.SetWaveformOptional,
161
+ target: this.mac,
162
+ payload: encodeSetWaveformOptional(hsbk16, duration, {
163
+ setHue: values.h !== undefined,
164
+ setSaturation: values.s !== undefined,
165
+ setBrightness: values.b !== undefined,
166
+ setKelvin: values.k !== undefined
167
+ }),
168
+ ackRequired: true,
169
+ resRequired: true
170
+ });
171
+ this.transport.send(this.ip, this.port, msg);
172
+ this.startRequestTimeout(MessageType.SetWaveformOptional);
145
173
  }
146
174
  /**
147
175
  * Set color - accepts flexible input formats
@@ -267,25 +295,28 @@ export class LxDevice {
267
295
  this.clearRequestTimeout(messageType);
268
296
  }
269
297
  /**
270
- * Check if message is a duplicate based on sequence number.
298
+ * Check if message is a duplicate based on sequence number AND type.
271
299
  * LIFX devices sometimes send duplicate responses.
300
+ * Different message types with same sequence are NOT duplicates (e.g., Ack + State).
272
301
  * @param sequence - Message sequence number
302
+ * @param type - Message type number
273
303
  * @returns true if duplicate (already seen recently)
274
304
  */
275
- isDuplicate(sequence) {
305
+ isDuplicate(sequence, type) {
276
306
  const now = Date.now();
277
307
  const DUPLICATE_WINDOW_MS = 2000; // 2 second window
308
+ const key = `${sequence}:${type}`;
278
309
  // Clean old entries
279
- for (const [seq, timestamp] of this.recentSequences.entries()) {
310
+ for (const [k, timestamp] of this.recentMessages.entries()) {
280
311
  if (now - timestamp > DUPLICATE_WINDOW_MS) {
281
- this.recentSequences.delete(seq);
312
+ this.recentMessages.delete(k);
282
313
  }
283
314
  }
284
- // Check if we've seen this sequence recently
285
- if (this.recentSequences.has(sequence)) {
315
+ // Check if we've seen this exact sequence+type recently
316
+ if (this.recentMessages.has(key)) {
286
317
  return true;
287
318
  }
288
- this.recentSequences.set(sequence, now);
319
+ this.recentMessages.set(key, now);
289
320
  return false;
290
321
  }
291
322
  }
package/index.d.ts CHANGED
@@ -3,5 +3,5 @@ export { LxDevice } from './device.js';
3
3
  export { LxClient, LxClientOptions } from './client.js';
4
4
  export { UdpTransport, LxTransport, RemoteInfo } from './transport.js';
5
5
  export { LxEventEmitter, LxEventEmitterBase } from './events.js';
6
- export { encodeMessage, decodeMessage } from './protocol.js';
6
+ export { encodeMessage, decodeMessage, encodeSetWifiConfiguration, encodeGetWifiConfiguration, decodeStateWifiConfiguration, encodeSetLabel, encodeSetColor, encodeSetPower, encodeSetGroup, encodeSetLocation, encodeSetWaveformOptional, encodeGetService, decodeState, decodeStatePower, decodeStateLabel, decodeStateVersion, decodeStateGroup, decodeStateService, decodeStateHostInfo, decodeStateHostFirmware, decodeStateWifiInfo, decodeStateInfo, FrameFlags, ProtocolBits, nextSequence, getSource, } from './protocol.js';
7
7
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -2,5 +2,5 @@ export * from './types.js';
2
2
  export { LxDevice } from './device.js';
3
3
  export { LxClient } from './client.js';
4
4
  export { LxEventEmitterBase } from './events.js';
5
- export { encodeMessage, decodeMessage } from './protocol.js';
5
+ export { encodeMessage, decodeMessage, encodeSetWifiConfiguration, encodeGetWifiConfiguration, decodeStateWifiConfiguration, encodeSetLabel, encodeSetColor, encodeSetPower, encodeSetGroup, encodeSetLocation, encodeSetWaveformOptional, encodeGetService, decodeState, decodeStatePower, decodeStateLabel, decodeStateVersion, decodeStateGroup, decodeStateService, decodeStateHostInfo, decodeStateHostFirmware, decodeStateWifiInfo, decodeStateInfo, FrameFlags, ProtocolBits, nextSequence, getSource, } from './protocol.js';
6
6
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/lxlan",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "LIFX LAN protocol library for device control via UDP",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -31,11 +31,14 @@
31
31
  "url": "https://github.com/BobFrankston/lxlan.git"
32
32
  },
33
33
  "dependencies": {
34
- "@bobfrankston/colorlib": "^0.1.7",
34
+ "@bobfrankston/colorlib": "^0.1.8",
35
35
  "@bobfrankston/udp-transport": "^1.0.2"
36
36
  },
37
37
  "devDependencies": {
38
- "@types/node": "^25.0.9"
38
+ "@types/node": "^25.2.1"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
39
42
  },
40
43
  ".dependencies": {
41
44
  "@bobfrankston/colorlib": "file:../../../../utils/colorlib",
package/protocol.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { HSBK16 } from '@bobfrankston/colorlib';
2
- import { LxMessage } from './types.js';
2
+ import { LxMessage, WifiConfigState } from './types.js';
3
3
  /** LIFX protocol frame flags */
4
4
  export declare const FrameFlags: {
5
5
  readonly ResRequired: 1;
@@ -31,6 +31,16 @@ export declare function decodeMessage(buf: Uint8Array): LxMessage;
31
31
  export declare function encodeSetPower(level: boolean): Uint8Array;
32
32
  /** Encode SetColor payload */
33
33
  export declare function encodeSetColor(hsbk: HSBK16, duration?: number): Uint8Array;
34
+ /** Encode SetWaveformOptional payload (type 119)
35
+ * Allows setting individual HSBK fields without affecting others.
36
+ * Set the corresponding set_* flag to true to apply that field.
37
+ */
38
+ export declare function encodeSetWaveformOptional(hsbk: HSBK16, duration?: number, flags?: {
39
+ setHue?: boolean;
40
+ setSaturation?: boolean;
41
+ setBrightness?: boolean;
42
+ setKelvin?: boolean;
43
+ }): Uint8Array;
34
44
  /** Encode SetLabel payload */
35
45
  export declare function encodeSetLabel(label: string): Uint8Array;
36
46
  /** Encode SetGroup payload */
@@ -89,4 +99,10 @@ export declare function decodeStateInfo(payload: Uint8Array): {
89
99
  };
90
100
  /** Encode GetService payload (empty) */
91
101
  export declare function encodeGetService(): Uint8Array;
102
+ /** Encode SetWifiConfiguration (Type 301) payload — 98 bytes */
103
+ export declare function encodeSetWifiConfiguration(ssid: string, pass: string, security?: number): Uint8Array;
104
+ /** Encode GetWifiConfiguration (Type 302) — no payload */
105
+ export declare function encodeGetWifiConfiguration(): Uint8Array;
106
+ /** Decode StateWifiConfiguration (Type 303) payload */
107
+ export declare function decodeStateWifiConfiguration(payload: Uint8Array): WifiConfigState;
92
108
  //# sourceMappingURL=protocol.d.ts.map
package/protocol.js CHANGED
@@ -1,4 +1,4 @@
1
- import { HEADER_SIZE } from './types.js';
1
+ import { HEADER_SIZE, WifiSecurity, WifiInterface } from './types.js';
2
2
  /** LIFX protocol frame flags */
3
3
  export const FrameFlags = {
4
4
  ResRequired: 0x01, /** Response required from device */
@@ -154,6 +154,30 @@ export function encodeSetColor(hsbk, duration = 0) {
154
154
  writeUInt32LE(buf, duration, 9);
155
155
  return buf;
156
156
  }
157
+ /** Encode SetWaveformOptional payload (type 119)
158
+ * Allows setting individual HSBK fields without affecting others.
159
+ * Set the corresponding set_* flag to true to apply that field.
160
+ */
161
+ export function encodeSetWaveformOptional(hsbk, duration = 0, flags = {}) {
162
+ const buf = new Uint8Array(25);
163
+ buf[0] = 0; // reserved
164
+ buf[1] = 0; // transient = false (permanent)
165
+ writeUInt16LE(buf, hsbk.h, 2); // hue
166
+ writeUInt16LE(buf, hsbk.s, 4); // saturation
167
+ writeUInt16LE(buf, hsbk.b, 6); // brightness
168
+ writeUInt16LE(buf, hsbk.k, 8); // kelvin
169
+ writeUInt32LE(buf, duration, 10); // period (ms) — used as transition duration
170
+ // cycles (float32) at offset 14 — 1.0 for single transition
171
+ const view = new DataView(buf.buffer, buf.byteOffset + 14, 4);
172
+ view.setFloat32(0, 1.0, true);
173
+ writeUInt16LE(buf, 0, 18); // skew_ratio = 0
174
+ buf[20] = 0; // waveform = SAW
175
+ buf[21] = flags.setHue ? 1 : 0; // set_hue
176
+ buf[22] = flags.setSaturation ? 1 : 0; // set_saturation
177
+ buf[23] = flags.setBrightness ? 1 : 0; // set_brightness
178
+ buf[24] = flags.setKelvin ? 1 : 0; // set_kelvin
179
+ return buf;
180
+ }
157
181
  /** Encode SetLabel payload */
158
182
  export function encodeSetLabel(label) {
159
183
  const buf = new Uint8Array(32);
@@ -256,6 +280,27 @@ export function decodeStateInfo(payload) {
256
280
  export function encodeGetService() {
257
281
  return new Uint8Array(0);
258
282
  }
283
+ /** Encode SetWifiConfiguration (Type 301) payload — 98 bytes */
284
+ export function encodeSetWifiConfiguration(ssid, pass, security = WifiSecurity.WPA2) {
285
+ const buf = new Uint8Array(98);
286
+ buf[0] = WifiInterface.Station;
287
+ utf8Encode(ssid, buf, 1, 32);
288
+ utf8Encode(pass, buf, 33, 64);
289
+ buf[97] = security;
290
+ return buf;
291
+ }
292
+ /** Encode GetWifiConfiguration (Type 302) — no payload */
293
+ export function encodeGetWifiConfiguration() {
294
+ return new Uint8Array(0);
295
+ }
296
+ /** Decode StateWifiConfiguration (Type 303) payload */
297
+ export function decodeStateWifiConfiguration(payload) {
298
+ return {
299
+ iface: payload[0],
300
+ ssid: utf8Decode(payload, 1, 33),
301
+ security: payload[97],
302
+ };
303
+ }
259
304
  // Buffer compatibility helper
260
305
  const BufferImpl = typeof Buffer !== 'undefined' ? Buffer : class {
261
306
  static alloc(size) {
package/types.d.ts CHANGED
@@ -26,6 +26,7 @@ export declare const MessageType: {
26
26
  readonly StateVersion: 33;
27
27
  readonly GetInfo: 34;
28
28
  readonly StateInfo: 35;
29
+ readonly Acknowledge: 45;
29
30
  readonly GetLocation: 48;
30
31
  readonly SetLocation: 49;
31
32
  readonly StateLocation: 50;
@@ -34,8 +35,30 @@ export declare const MessageType: {
34
35
  readonly StateGroup: 53;
35
36
  readonly Get: 101;
36
37
  readonly SetColor: 102;
38
+ readonly SetWaveformOptional: 119;
37
39
  readonly State: 107;
40
+ readonly SetWifiConfiguration: 301;
41
+ readonly GetWifiConfiguration: 302;
42
+ readonly StateWifiConfiguration: 303;
38
43
  };
44
+ /** Wi-Fi security modes for onboarding */
45
+ export declare const WifiSecurity: {
46
+ readonly Open: 1;
47
+ readonly WEP: 2;
48
+ readonly WPA: 3;
49
+ readonly WPA2: 4;
50
+ };
51
+ /** Wi-Fi interface type */
52
+ export declare const WifiInterface: {
53
+ readonly Station: 6;
54
+ };
55
+ /** Wi-Fi configuration state (from Type 303 response) */
56
+ export interface WifiConfigState {
57
+ iface: number; /** Interface type (6 = Station) */
58
+ ssid: string;
59
+ security: number;
60
+ }
61
+ export declare function MessageTypeName(type: number): string;
39
62
  export type MessageTypeValue = typeof MessageType[keyof typeof MessageType];
40
63
  /** LIFX message structure */
41
64
  export interface LxMessage {
@@ -85,6 +108,11 @@ export interface DeviceState {
85
108
  uptime?: number;
86
109
  downtime?: number;
87
110
  }
111
+ /** Info passed with state events */
112
+ export interface StateEventInfo {
113
+ /** true if from device response, false if from peer client broadcast */
114
+ primary: boolean;
115
+ }
88
116
  /** LIFX port */
89
117
  export declare const LIFX_PORT = 56700;
90
118
  /** Header size */
package/types.js CHANGED
@@ -18,6 +18,7 @@ export const MessageType = {
18
18
  StateVersion: 33,
19
19
  GetInfo: 34,
20
20
  StateInfo: 35,
21
+ Acknowledge: 45,
21
22
  GetLocation: 48,
22
23
  SetLocation: 49,
23
24
  StateLocation: 50,
@@ -26,8 +27,31 @@ export const MessageType = {
26
27
  StateGroup: 53,
27
28
  Get: 101, /** GetColor */
28
29
  SetColor: 102,
30
+ SetWaveformOptional: 119, /** Set HSBK with per-field control flags */
29
31
  State: 107, /** LightState */
32
+ SetWifiConfiguration: 301, /** Onboarding: set Wi-Fi credentials */
33
+ GetWifiConfiguration: 302, /** Onboarding: query current Wi-Fi config */
34
+ StateWifiConfiguration: 303, /** Onboarding: response with current Wi-Fi config */
30
35
  };
36
+ /** Wi-Fi security modes for onboarding */
37
+ export const WifiSecurity = {
38
+ Open: 1,
39
+ WEP: 2,
40
+ WPA: 3,
41
+ WPA2: 4,
42
+ };
43
+ /** Wi-Fi interface type */
44
+ export const WifiInterface = {
45
+ Station: 6, /** Station mode (join existing network) */
46
+ };
47
+ // This is for debuging so it doesn't have to be that
48
+ export function MessageTypeName(type) {
49
+ for (const [name, val] of Object.entries(MessageType)) {
50
+ if (val === type)
51
+ return name;
52
+ }
53
+ return `Unknown(${type})`;
54
+ }
31
55
  /** LIFX port */
32
56
  export const LIFX_PORT = 56700;
33
57
  /** Header size */