-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.ts
124 lines (103 loc) · 3.24 KB
/
main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import { DeviceDescriptor, enumerate, OpaqueDevice, open, write } from 'hid';
/**
* The message for turning the light on.
*/
export const ON = Buffer.from([0, 0x11, 0xff, 0x04, 0x1d, 0x01]);
/**
* The message for turning the light off.
*/
export const OFF = Buffer.from([0, 0x11, 0xff, 0x04, 0x1d, 0x00]);
export const WARM = 2700;
export const COOL = 6500;
export const NEUTRAL = (COOL - WARM) / 2 + WARM;
export class LitraBeam {
/**
* A Litra Beam device descriptor
*/
#descriptor?: DeviceDescriptor;
/**
* A handle to the specific device
*/
#device: OpaqueDevice;
constructor(private readonly serialNumber?: string) {
// Get all HIDs and look for one with Litra's vendor ID and product ID.
// Then filter that down to the one with a specific serial number, if given,
// or the first Litra found.
this.#descriptor = this.getDescriptors().find((device) => {
if (this.serialNumber) {
return device.serial_number === this.serialNumber;
}
return true;
});
if (!this.#descriptor) {
throw new Error('Litra Beam not connected');
}
this.#device = open(
this.#descriptor.vendor_id,
this.#descriptor.product_id,
this.#descriptor.serial_number
);
}
/**
* Gets descriptors for all connected Litra Beams
*/
public getDescriptors() {
return enumerate().filter(
(descriptor) =>
descriptor.product_string === 'Litra Beam' &&
Boolean(descriptor.serial_number)
);
}
public on(): void {
write(this.#device, ON);
}
public off(): void {
write(this.#device, OFF);
}
public setBrightness(value: number): void {
const translatedValue = Math.round((value * (255 - 30)) / 255 + 30);
write(
this.#device,
Buffer.from([0x11, 0xff, 0x04, 0x4f, 0x00, translatedValue])
);
}
/**
* Set the temperature to a value between the 2700 (warm) and 6500 (cool).
* The device only accepts values rounded to the nearest hundred, so any
* provided will be rounded.
*/
public setTemperature(temperature: number): void {
const temp = Math.min(
Math.max(
temperature || NEUTRAL, // Use neutral if NaN
WARM // At least 2700
),
COOL // at most 6500
);
// Convert the number to 16 bit, and pad it with 0 if necessary.
// 2700 → a8c → 0A8C
const hexTemperature = temp.toString(16).padStart(4, '0').toUpperCase();
// Split the four-character number into two two-character numbers.
// 0A8C → 0A,8C
const first = parseInt(hexTemperature.slice(0, 2), 16);
const second = parseInt(hexTemperature.slice(2), 16);
write(this.#device, Buffer.from([0x11, 0xff, 0x04, 0x9d, first, second]));
}
/**
* Set the temperature to a percentage between the 2700 (warm) and 6500 (cool)
*/
public setTemperaturePercentage(percent: number): void {
const constrainedPercentage = Math.min(
Math.max(
Number.isNaN(percent) ? 50 : percent, // Use neutral if NaN
0
),
100
);
const range = COOL - WARM;
const step = range / 100;
const temperature = constrainedPercentage * step + WARM;
const temperatureToNearest100 = Math.round(temperature / 100) * 100;
this.setTemperature(temperatureToNearest100);
}
}