@bobfrankston/lxlan 0.1.0

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.
@@ -0,0 +1,13 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm install:*)",
5
+ "Bash(tsc:*)",
6
+ "Bash(git init:*)",
7
+ "Bash(git add:*)",
8
+ "Bash(git commit:*)",
9
+ "Bash(gh repo create:*)",
10
+ "Bash(npm publish:*)"
11
+ ]
12
+ }
13
+ }
package/client.d.ts ADDED
@@ -0,0 +1,65 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { UdpRetryOptions } from '@bobfrankston/lxudp';
3
+ import { LxDevice } from './device.js';
4
+ /**
5
+ * Options for LxClient constructor
6
+ */
7
+ export interface LxClientOptions {
8
+ /** UDP port to bind (default 56700) */
9
+ port?: number;
10
+ /** Broadcast addresses for discovery (auto-detected from network interfaces if not specified) */
11
+ broadcastAddresses?: string[];
12
+ /** Auto-discovery interval in ms (0 = manual only, default 0) */
13
+ discoveryInterval?: number;
14
+ }
15
+ /**
16
+ * LIFX LAN client for device discovery and control.
17
+ *
18
+ * Events:
19
+ * - `device` (device: LxDevice) - new device discovered
20
+ * - `message` (device: LxDevice, msg: LxMessage) - any message received
21
+ * - `state` (device: LxDevice) - light state updated
22
+ * - `power` (device: LxDevice) - power state updated
23
+ * - `label` (device: LxDevice) - label updated
24
+ * - `group` (device: LxDevice) - group info updated
25
+ * - `location` (device: LxDevice) - location info updated
26
+ * - `version` (device: LxDevice) - version info received
27
+ * - `error` (err: Error) - transport or protocol error
28
+ */
29
+ export declare class LxClient extends EventEmitter {
30
+ private socket;
31
+ private port;
32
+ private broadcastAddresses;
33
+ private discoveryTimer;
34
+ /** Cached devices by MAC address (lowercase) */
35
+ devices: Map<string, LxDevice>;
36
+ constructor(options?: LxClientOptions);
37
+ /** Start listening */
38
+ start(): Promise<void>;
39
+ /** Stop and cleanup */
40
+ stop(): void;
41
+ /** Set retry options at runtime */
42
+ setRetryOptions(options: UdpRetryOptions): void;
43
+ /** Get current retry options */
44
+ getRetryOptions(): Required<UdpRetryOptions>;
45
+ /** Broadcast discovery to all interfaces */
46
+ discover(): void;
47
+ /**
48
+ * Get device by MAC address
49
+ * @param mac - MAC address (case-insensitive, with or without colons)
50
+ * @returns LxDevice or undefined if not found
51
+ */
52
+ getDevice(mac: string): LxDevice;
53
+ /**
54
+ * Add device manually (bypassing discovery).
55
+ * Use when you know the device IP from external source.
56
+ * @param mac - MAC address
57
+ * @param ip - IP address
58
+ * @param port - UDP port (default 56700)
59
+ * @returns LxDevice instance (new or existing)
60
+ */
61
+ addDevice(mac: string, ip: string, port?: number): LxDevice;
62
+ /** Handle incoming message */
63
+ private handleMessage;
64
+ }
65
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAa,eAAe,EAAyB,MAAM,qBAAqB,CAAC;AACxF,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAIvC;;GAEG;AACH,MAAM,WAAW,eAAe;IAC5B,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iGAAiG;IACjG,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,iEAAiE;IACjE,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,QAAS,SAAQ,YAAY;IACtC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,kBAAkB,CAAW;IACrC,OAAO,CAAC,cAAc,CAAiB;IAEvC,gDAAgD;IAChD,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAa;gBAE/B,OAAO,GAAE,eAAoB;IAuBzC,sBAAsB;IAChB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B,uBAAuB;IACvB,IAAI,IAAI,IAAI;IAKZ,mCAAmC;IACnC,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAI/C,gCAAgC;IAChC,eAAe,IAAI,QAAQ,CAAC,eAAe,CAAC;IAI5C,4CAA4C;IAC5C,QAAQ,IAAI,IAAI;IAWhB;;;;OAIG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ;IAIhC;;;;;;;OAOG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG,QAAQ;IAiBtE,8BAA8B;IAC9B,OAAO,CAAC,aAAa;CAgFxB"}
package/client.js ADDED
@@ -0,0 +1,176 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { hsbk16ToHsbk } from '@bobfrankston/colorlib';
3
+ import { UdpSocket, getBroadcastAddresses } from '@bobfrankston/lxudp';
4
+ import { LxDevice } from './device.js';
5
+ import { MessageType, LIFX_PORT } from './types.js';
6
+ import { encodeMessage, decodeMessage, decodeState, decodeStatePower, decodeStateLabel, decodeStateVersion, decodeStateGroup, decodeStateService } from './protocol.js';
7
+ /**
8
+ * LIFX LAN client for device discovery and control.
9
+ *
10
+ * Events:
11
+ * - `device` (device: LxDevice) - new device discovered
12
+ * - `message` (device: LxDevice, msg: LxMessage) - any message received
13
+ * - `state` (device: LxDevice) - light state updated
14
+ * - `power` (device: LxDevice) - power state updated
15
+ * - `label` (device: LxDevice) - label updated
16
+ * - `group` (device: LxDevice) - group info updated
17
+ * - `location` (device: LxDevice) - location info updated
18
+ * - `version` (device: LxDevice) - version info received
19
+ * - `error` (err: Error) - transport or protocol error
20
+ */
21
+ export class LxClient extends EventEmitter {
22
+ socket;
23
+ port;
24
+ broadcastAddresses;
25
+ discoveryTimer;
26
+ /** Cached devices by MAC address (lowercase) */
27
+ devices = new Map();
28
+ constructor(options = {}) {
29
+ super();
30
+ this.port = options.port ?? LIFX_PORT;
31
+ this.broadcastAddresses = options.broadcastAddresses ?? getBroadcastAddresses();
32
+ this.socket = new UdpSocket({
33
+ port: this.port,
34
+ reuseAddr: true
35
+ });
36
+ this.socket.on('message', (data, rinfo) => {
37
+ this.handleMessage(data, rinfo.address, rinfo.port);
38
+ });
39
+ this.socket.on('error', (err) => {
40
+ this.emit('error', err);
41
+ });
42
+ if (options.discoveryInterval && options.discoveryInterval > 0) {
43
+ this.discoveryTimer = setInterval(() => this.discover(), options.discoveryInterval);
44
+ }
45
+ }
46
+ /** Start listening */
47
+ async start() {
48
+ await this.socket.bind();
49
+ }
50
+ /** Stop and cleanup */
51
+ stop() {
52
+ if (this.discoveryTimer)
53
+ clearInterval(this.discoveryTimer);
54
+ this.socket.close();
55
+ }
56
+ /** Set retry options at runtime */
57
+ setRetryOptions(options) {
58
+ this.socket.setRetryOptions(options);
59
+ }
60
+ /** Get current retry options */
61
+ getRetryOptions() {
62
+ return this.socket.getRetryOptions();
63
+ }
64
+ /** Broadcast discovery to all interfaces */
65
+ discover() {
66
+ const msg = encodeMessage({
67
+ type: MessageType.GetService,
68
+ tagged: true
69
+ });
70
+ for (const addr of this.broadcastAddresses) {
71
+ this.socket.send(addr, this.port, msg);
72
+ }
73
+ }
74
+ /**
75
+ * Get device by MAC address
76
+ * @param mac - MAC address (case-insensitive, with or without colons)
77
+ * @returns LxDevice or undefined if not found
78
+ */
79
+ getDevice(mac) {
80
+ return this.devices.get(mac.toLowerCase());
81
+ }
82
+ /**
83
+ * Add device manually (bypassing discovery).
84
+ * Use when you know the device IP from external source.
85
+ * @param mac - MAC address
86
+ * @param ip - IP address
87
+ * @param port - UDP port (default 56700)
88
+ * @returns LxDevice instance (new or existing)
89
+ */
90
+ addDevice(mac, ip, port = LIFX_PORT) {
91
+ const normalizedMac = mac.toLowerCase();
92
+ let device = this.devices.get(normalizedMac);
93
+ if (!device) {
94
+ device = new LxDevice(normalizedMac, ip, this.socket);
95
+ device.port = port;
96
+ this.devices.set(normalizedMac, device);
97
+ this.emit('device', device);
98
+ }
99
+ else {
100
+ device.ip = ip;
101
+ device.port = port;
102
+ }
103
+ return device;
104
+ }
105
+ /** Handle incoming message */
106
+ handleMessage(data, fromIp, fromPort) {
107
+ let msg;
108
+ try {
109
+ msg = decodeMessage(data);
110
+ }
111
+ catch (e) {
112
+ this.emit('error', new Error(`Decode error: ${e.message}`));
113
+ return;
114
+ }
115
+ const mac = msg.target;
116
+ if (mac === '00:00:00:00:00:00')
117
+ return; /** broadcast response without target */
118
+ let device = this.devices.get(mac);
119
+ const isNew = !device;
120
+ if (!device) {
121
+ device = new LxDevice(mac, fromIp, this.socket);
122
+ device.port = fromPort;
123
+ this.devices.set(mac, device);
124
+ }
125
+ device.ip = fromIp;
126
+ device.port = fromPort;
127
+ device.markSeen();
128
+ if (isNew) {
129
+ this.emit('device', device);
130
+ }
131
+ this.emit('message', device, msg);
132
+ switch (msg.type) {
133
+ case MessageType.StateService: {
134
+ const info = decodeStateService(msg.payload);
135
+ device.port = info.port;
136
+ break;
137
+ }
138
+ case MessageType.State: {
139
+ const state = decodeState(msg.payload);
140
+ device.color = hsbk16ToHsbk(state.hsbk);
141
+ device.power = state.power > 0;
142
+ device.label = state.label;
143
+ this.emit('state', device);
144
+ break;
145
+ }
146
+ case MessageType.StatePower: {
147
+ device.power = decodeStatePower(msg.payload);
148
+ this.emit('power', device);
149
+ break;
150
+ }
151
+ case MessageType.StateLabel: {
152
+ device.label = decodeStateLabel(msg.payload);
153
+ this.emit('label', device);
154
+ break;
155
+ }
156
+ case MessageType.StateVersion: {
157
+ const ver = decodeStateVersion(msg.payload);
158
+ device.vendor = ver.vendor;
159
+ device.product = ver.product;
160
+ this.emit('version', device);
161
+ break;
162
+ }
163
+ case MessageType.StateGroup: {
164
+ device.group = decodeStateGroup(msg.payload);
165
+ this.emit('group', device);
166
+ break;
167
+ }
168
+ case MessageType.StateLocation: {
169
+ device.location = decodeStateGroup(msg.payload);
170
+ this.emit('location', device);
171
+ break;
172
+ }
173
+ }
174
+ }
175
+ }
176
+ //# sourceMappingURL=client.js.map
package/client.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["client.ts"],"names":[],"mappings":"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"}
package/client.ts ADDED
@@ -0,0 +1,214 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { hsbk16ToHsbk } from '@bobfrankston/colorlib';
3
+ import { UdpSocket, UdpRetryOptions, getBroadcastAddresses } from '@bobfrankston/lxudp';
4
+ import { LxDevice } from './device.js';
5
+ import { LxMessage, MessageType, LIFX_PORT } from './types.js';
6
+ import { encodeMessage, decodeMessage, decodeState, decodeStatePower, decodeStateLabel, decodeStateVersion, decodeStateGroup, decodeStateService } from './protocol.js';
7
+
8
+ /**
9
+ * Options for LxClient constructor
10
+ */
11
+ export interface LxClientOptions {
12
+ /** UDP port to bind (default 56700) */
13
+ port?: number;
14
+ /** Broadcast addresses for discovery (auto-detected from network interfaces if not specified) */
15
+ broadcastAddresses?: string[];
16
+ /** Auto-discovery interval in ms (0 = manual only, default 0) */
17
+ discoveryInterval?: number;
18
+ }
19
+
20
+ /**
21
+ * LIFX LAN client for device discovery and control.
22
+ *
23
+ * Events:
24
+ * - `device` (device: LxDevice) - new device discovered
25
+ * - `message` (device: LxDevice, msg: LxMessage) - any message received
26
+ * - `state` (device: LxDevice) - light state updated
27
+ * - `power` (device: LxDevice) - power state updated
28
+ * - `label` (device: LxDevice) - label updated
29
+ * - `group` (device: LxDevice) - group info updated
30
+ * - `location` (device: LxDevice) - location info updated
31
+ * - `version` (device: LxDevice) - version info received
32
+ * - `error` (err: Error) - transport or protocol error
33
+ */
34
+ export class LxClient extends EventEmitter {
35
+ private socket: UdpSocket;
36
+ private port: number;
37
+ private broadcastAddresses: string[];
38
+ private discoveryTimer: NodeJS.Timeout;
39
+
40
+ /** Cached devices by MAC address (lowercase) */
41
+ devices: Map<string, LxDevice> = new Map();
42
+
43
+ constructor(options: LxClientOptions = {}) {
44
+ super();
45
+ this.port = options.port ?? LIFX_PORT;
46
+ this.broadcastAddresses = options.broadcastAddresses ?? getBroadcastAddresses();
47
+
48
+ this.socket = new UdpSocket({
49
+ port: this.port,
50
+ reuseAddr: true
51
+ });
52
+
53
+ this.socket.on('message', (data, rinfo) => {
54
+ this.handleMessage(data, rinfo.address, rinfo.port);
55
+ });
56
+
57
+ this.socket.on('error', (err) => {
58
+ this.emit('error', err);
59
+ });
60
+
61
+ if (options.discoveryInterval && options.discoveryInterval > 0) {
62
+ this.discoveryTimer = setInterval(() => this.discover(), options.discoveryInterval);
63
+ }
64
+ }
65
+
66
+ /** Start listening */
67
+ async start(): Promise<void> {
68
+ await this.socket.bind();
69
+ }
70
+
71
+ /** Stop and cleanup */
72
+ stop(): void {
73
+ if (this.discoveryTimer) clearInterval(this.discoveryTimer);
74
+ this.socket.close();
75
+ }
76
+
77
+ /** Set retry options at runtime */
78
+ setRetryOptions(options: UdpRetryOptions): void {
79
+ this.socket.setRetryOptions(options);
80
+ }
81
+
82
+ /** Get current retry options */
83
+ getRetryOptions(): Required<UdpRetryOptions> {
84
+ return this.socket.getRetryOptions();
85
+ }
86
+
87
+ /** Broadcast discovery to all interfaces */
88
+ discover(): void {
89
+ const msg = encodeMessage({
90
+ type: MessageType.GetService,
91
+ tagged: true
92
+ });
93
+
94
+ for (const addr of this.broadcastAddresses) {
95
+ this.socket.send(addr, this.port, msg);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Get device by MAC address
101
+ * @param mac - MAC address (case-insensitive, with or without colons)
102
+ * @returns LxDevice or undefined if not found
103
+ */
104
+ getDevice(mac: string): LxDevice {
105
+ return this.devices.get(mac.toLowerCase());
106
+ }
107
+
108
+ /**
109
+ * Add device manually (bypassing discovery).
110
+ * Use when you know the device IP from external source.
111
+ * @param mac - MAC address
112
+ * @param ip - IP address
113
+ * @param port - UDP port (default 56700)
114
+ * @returns LxDevice instance (new or existing)
115
+ */
116
+ addDevice(mac: string, ip: string, port: number = LIFX_PORT): LxDevice {
117
+ const normalizedMac = mac.toLowerCase();
118
+ let device = this.devices.get(normalizedMac);
119
+
120
+ if (!device) {
121
+ device = new LxDevice(normalizedMac, ip, this.socket);
122
+ device.port = port;
123
+ this.devices.set(normalizedMac, device);
124
+ this.emit('device', device);
125
+ } else {
126
+ device.ip = ip;
127
+ device.port = port;
128
+ }
129
+
130
+ return device;
131
+ }
132
+
133
+ /** Handle incoming message */
134
+ private handleMessage(data: Buffer, fromIp: string, fromPort: number): void {
135
+ let msg: LxMessage;
136
+ try {
137
+ msg = decodeMessage(data);
138
+ } catch (e: any) {
139
+ this.emit('error', new Error(`Decode error: ${e.message}`));
140
+ return;
141
+ }
142
+
143
+ const mac = msg.target;
144
+ if (mac === '00:00:00:00:00:00') return; /** broadcast response without target */
145
+
146
+ let device = this.devices.get(mac);
147
+ const isNew = !device;
148
+
149
+ if (!device) {
150
+ device = new LxDevice(mac, fromIp, this.socket);
151
+ device.port = fromPort;
152
+ this.devices.set(mac, device);
153
+ }
154
+
155
+ device.ip = fromIp;
156
+ device.port = fromPort;
157
+ device.markSeen();
158
+
159
+ if (isNew) {
160
+ this.emit('device', device);
161
+ }
162
+
163
+ this.emit('message', device, msg);
164
+
165
+ switch (msg.type) {
166
+ case MessageType.StateService: {
167
+ const info = decodeStateService(msg.payload);
168
+ device.port = info.port;
169
+ break;
170
+ }
171
+
172
+ case MessageType.State: {
173
+ const state = decodeState(msg.payload);
174
+ device.color = hsbk16ToHsbk(state.hsbk);
175
+ device.power = state.power > 0;
176
+ device.label = state.label;
177
+ this.emit('state', device);
178
+ break;
179
+ }
180
+
181
+ case MessageType.StatePower: {
182
+ device.power = decodeStatePower(msg.payload);
183
+ this.emit('power', device);
184
+ break;
185
+ }
186
+
187
+ case MessageType.StateLabel: {
188
+ device.label = decodeStateLabel(msg.payload);
189
+ this.emit('label', device);
190
+ break;
191
+ }
192
+
193
+ case MessageType.StateVersion: {
194
+ const ver = decodeStateVersion(msg.payload);
195
+ device.vendor = ver.vendor;
196
+ device.product = ver.product;
197
+ this.emit('version', device);
198
+ break;
199
+ }
200
+
201
+ case MessageType.StateGroup: {
202
+ device.group = decodeStateGroup(msg.payload);
203
+ this.emit('group', device);
204
+ break;
205
+ }
206
+
207
+ case MessageType.StateLocation: {
208
+ device.location = decodeStateGroup(msg.payload);
209
+ this.emit('location', device);
210
+ break;
211
+ }
212
+ }
213
+ }
214
+ }
package/device.d.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { HSBK, ColorInput } from '@bobfrankston/colorlib';
2
+ import { UdpSocket } from '@bobfrankston/lxudp';
3
+ import { GroupInfo } from './types.js';
4
+ /**
5
+ * Represents a LIFX device on the LAN.
6
+ * State is cached and updated via events from LxClient.
7
+ */
8
+ export declare class LxDevice {
9
+ /** MAC address (lowercase, colon-separated) */
10
+ readonly mac: string;
11
+ /** Current IP address */
12
+ ip: string;
13
+ /** UDP port (default 56700) */
14
+ port: number;
15
+ /** Power state (true = on) */
16
+ power: boolean;
17
+ /** Current color in HSBK format */
18
+ color: HSBK;
19
+ /** Device label (user-assigned name) */
20
+ label: string;
21
+ /** Whether device is responding */
22
+ online: boolean;
23
+ /** Last message timestamp (Date.now()) */
24
+ lastSeen: number;
25
+ /** Vendor ID (1 = LIFX) */
26
+ vendor: number;
27
+ /** Product ID (see Products table) */
28
+ product: number;
29
+ /** Group membership */
30
+ group: GroupInfo;
31
+ /** Location membership */
32
+ location: GroupInfo;
33
+ private socket;
34
+ constructor(mac: string, ip: string, socket: UdpSocket);
35
+ /** Get product name from product ID */
36
+ get productName(): string;
37
+ /** Send raw message to device */
38
+ send(type: number, payload?: Buffer): void;
39
+ /** Set power on/off */
40
+ setPower(on: boolean): void;
41
+ /**
42
+ * Set color - accepts flexible input formats
43
+ * @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
44
+ * @param duration - Transition time in milliseconds (default 0)
45
+ */
46
+ setColor(color: ColorInput, duration?: number): void;
47
+ /**
48
+ * Set white color temperature
49
+ * @param kelvin - Color temperature (1500-9000, warm to cool)
50
+ * @param brightness - Brightness 0-100 (default 100)
51
+ * @param duration - Transition time in milliseconds (default 0)
52
+ */
53
+ setWhite(kelvin: number, brightness?: number, duration?: number): void;
54
+ /** Query full state */
55
+ getState(): void;
56
+ /** Query power state */
57
+ getPower(): void;
58
+ /** Set label */
59
+ setLabel(label: string): void;
60
+ /** Query label */
61
+ getLabel(): void;
62
+ /** Set group */
63
+ setGroup(id: string, label: string): void;
64
+ /** Query group */
65
+ getGroup(): void;
66
+ /** Set location */
67
+ setLocation(id: string, label: string): void;
68
+ /** Query location */
69
+ getLocation(): void;
70
+ /** Query version */
71
+ getVersion(): void;
72
+ /** Update last seen timestamp and online status */
73
+ markSeen(): void;
74
+ }
75
+ //# sourceMappingURL=device.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.d.ts","sourceRoot":"","sources":["device.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAwB,MAAM,wBAAwB,CAAC;AAChF,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAoC,MAAM,YAAY,CAAC;AAGzE;;;GAGG;AACH,qBAAa,QAAQ;IACjB,+CAA+C;IAC/C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,yBAAyB;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAa;IAEzB,8BAA8B;IAC9B,KAAK,EAAE,OAAO,CAAS;IACvB,mCAAmC;IACnC,KAAK,EAAE,IAAI,CAAiC;IAC5C,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAM;IACnB,mCAAmC;IACnC,MAAM,EAAE,OAAO,CAAS;IACxB,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAK;IAErB,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAK;IACnB,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAK;IAEpB,uBAAuB;IACvB,KAAK,EAAE,SAAS,CAAuC;IACvD,0BAA0B;IAC1B,QAAQ,EAAE,SAAS,CAAuC;IAE1D,OAAO,CAAC,MAAM,CAAY;gBAEd,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS;IAMtD,uCAAuC;IACvC,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,iCAAiC;IACjC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAW1C,uBAAuB;IACvB,QAAQ,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI;IAU3B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,GAAE,MAAU,GAAG,IAAI;IAYvD;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,GAAE,MAAY,EAAE,QAAQ,GAAE,MAAU,GAAG,IAAI;IAI9E,uBAAuB;IACvB,QAAQ,IAAI,IAAI;IAIhB,wBAAwB;IACxB,QAAQ,IAAI,IAAI;IAIhB,gBAAgB;IAChB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAU7B,kBAAkB;IAClB,QAAQ,IAAI,IAAI;IAIhB,gBAAgB;IAChB,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAUzC,kBAAkB;IAClB,QAAQ,IAAI,IAAI;IAIhB,mBAAmB;IACnB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAU5C,qBAAqB;IACrB,WAAW,IAAI,IAAI;IAInB,oBAAoB;IACpB,UAAU,IAAI,IAAI;IAIlB,mDAAmD;IACnD,QAAQ,IAAI,IAAI;CAInB"}