Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add basic support for the Logitech Litra Beam LX #294

Merged
merged 1 commit into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Logitech Litra

This JavaScript driver allows you to control USB-connected [Logitech Litra Glow](https://www.logitech.com/en-gb/products/lighting/litra-glow.946-000002.html) and [Logitech Litra Beam](https://www.logitech.com/en-gb/products/lighting/litra-beam.946-000007.html) lights using a CLI and from your JavaScript code.
This driver allows you to control USB-connected Logitech Litra lights using a CLI or from JavaScript code.

The following Logitech Litra devices are supported:

* [Logitech Litra Glow](https://www.logitech.com/en-gb/products/lighting/litra-glow.946-000002.html)
* [Logitech Litra Beam](https://www.logitech.com/en-gb/products/lighting/litra-beam.946-000007.html)
* [Logitech Litra Beam LX](https://www.logitechg.com/en-gb/products/cameras-lighting/litra-beam-lx-led-light.946-000015.html?&utm_source=Google&utm_medium=Paid-Search&utm_campaign=Dialect_FY24_Q3_GBR_GA_G_DTX-LogiG-Creator_Google_na&gad_source=1&gclid=CjwKCAiAp5qsBhAPEiwAP0qeJs7jOdlBu8DCsEoOFt1_BK1HLABI0l2jglDweTnNDddt5neXm_vpyRoCic4QAvD_BwE)

With this driver, you can:

Expand All @@ -13,7 +19,7 @@ With this driver, you can:

This library:

* only works with Litra devices connected via USB. Logitech Litra Beam devices connected via Bluetooth are not supported.
* only works with Litra devices connected via USB. Devices connected via Bluetooth are not supported.
* is only tested on macOS Monterey (12.5) and Windows 11. It's powered by [`node-hid`](https://github.com/node-hid/node-hid), which is compatible with other macOS versions, Windows and Linux, so it would be expected to work there too, but your mileage may vary 馃檹

## Using as a command line tool
Expand Down Expand Up @@ -135,7 +141,7 @@ You can set the brightness of your Litra device, measured in Lumen, using the `s

To get the current brightness of your device, use the `getBrightnessInLumen` function.

The Litra Glow supports brightness between 20 and 250 Lumen. The Litra Beam supports brightness between 20 and 400 Lumen.
The Litra Glow supports brightness between 20 and 250 Lumen. The Litra Beam and Litra Beam LX support brightness between 20 and 400 Lumen.

You can programatically check what brightness levels are supported by your device. Once you know what brightness levels are supported, you can set the brightness in Lumen. If you try to set a value that isn't allowed by your device, an error will be thrown:

Expand Down Expand Up @@ -182,7 +188,7 @@ You can set the temperature of your Litra device, measured in Kelvin, using the

The `getTemperatureInKelvin` function can be used to get the current temperature your device is set to.

Both the Litra Glow and Litra Beam support temperatures which are multiples of 100 between 2700 and 6500 Kelvin (i.e.. 2700, 2800, 2900, etc.).
All supported Litra devices support temperatures which are multiples of 100 between 2700 and 6500 Kelvin (i.e.. 2700, 2800, 2900, etc.).

You can check programatically what temperature levels are supported by your device. Once you know what temperature levels are supported, you can set the temperature in Kelvin. If you try to set a value that isn't allowed by your device, an error will be thrown:

Expand Down
3 changes: 2 additions & 1 deletion dist/commonjs/driver.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/// <reference types="node" />
export declare enum DeviceType {
LitraGlow = "litra_glow",
LitraBeam = "litra_beam"
LitraBeam = "litra_beam",
LitraBeamLX = "litra_beam_lx"
}
export interface Device {
hid: {
Expand Down
87 changes: 79 additions & 8 deletions dist/commonjs/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,36 @@ var DeviceType;
(function (DeviceType) {
DeviceType["LitraGlow"] = "litra_glow";
DeviceType["LitraBeam"] = "litra_beam";
DeviceType["LitraBeamLX"] = "litra_beam_lx";
})(DeviceType || (exports.DeviceType = DeviceType = {}));
const VENDOR_ID = 0x046d;
const PRODUCT_IDS = [
0xc900,
0xc901,
0xb901, // Litra Beam
0xb901,
0xc903, // Litra Beam LX
];
const USAGE_PAGE = 0xff43;
const MINIMUM_BRIGHTNESS_IN_LUMEN_BY_DEVICE_TYPE = {
[DeviceType.LitraGlow]: 20,
[DeviceType.LitraBeam]: 30,
[DeviceType.LitraBeamLX]: 30,
};
const MAXIMUM_BRIGHTNESS_IN_LUMEN_BY_DEVICE_TYPE = {
[DeviceType.LitraGlow]: 250,
[DeviceType.LitraBeam]: 400,
[DeviceType.LitraBeamLX]: 400,
};
const MULTIPLES_OF_100_BETWEEN_2700_AND_6500 = (0, utils_1.multiplesWithinRange)(100, 2700, 6500);
const ALLOWED_TEMPERATURES_IN_KELVIN_BY_DEVICE_TYPE = {
[DeviceType.LitraGlow]: MULTIPLES_OF_100_BETWEEN_2700_AND_6500,
[DeviceType.LitraBeam]: MULTIPLES_OF_100_BETWEEN_2700_AND_6500,
[DeviceType.LitraBeamLX]: MULTIPLES_OF_100_BETWEEN_2700_AND_6500,
};
const NAME_BY_DEVICE_TYPE = {
[DeviceType.LitraGlow]: 'Logitech Litra Glow',
[DeviceType.LitraBeam]: 'Logitech Litra Beam',
[DeviceType.LitraBeamLX]: 'Logitech Litra Beam LX',
};
const isLitraDevice = (device) => {
return (device.vendorId === VENDOR_ID &&
Expand Down Expand Up @@ -79,22 +85,40 @@ const findDevices = () => {
return matchingDevices.map(hidDeviceToDevice);
};
exports.findDevices = findDevices;
const generateTurnOnBytes = (device) => {
if (device.type === DeviceType.LitraBeamLX) {
return (0, utils_1.padRight)([0x11, 0xff, 0x06, 0x1c, 0x01], 20, 0x00);
}
else {
return (0, utils_1.padRight)([0x11, 0xff, 0x04, 0x1c, 0x01], 20, 0x00);
}
};
/**
* Turns your Logitech Litra device on
*
* @param {Device} device The device to turn on
*/
const turnOn = (device) => {
device.hid.write((0, utils_1.padRight)([0x11, 0xff, 0x04, 0x1c, 0x01], 20, 0x00));
const bytes = generateTurnOnBytes(device);
device.hid.write(bytes);
};
exports.turnOn = turnOn;
const generateTurnOffBytes = (device) => {
if (device.type === DeviceType.LitraBeamLX) {
return (0, utils_1.padRight)([0x11, 0xff, 0x06, 0x1c, 0x00], 20, 0x00);
}
else {
return (0, utils_1.padRight)([0x11, 0xff, 0x04, 0x1c, 0x00], 20, 0x00);
}
};
/**
* Turns your Logitech Litra device off
*
* @param {Device} device The device to turn off
*/
const turnOff = (device) => {
device.hid.write((0, utils_1.padRight)([0x11, 0xff, 0x04, 0x1c, 0x00], 20, 0x00));
const bytes = generateTurnOffBytes(device);
device.hid.write(bytes);
};
exports.turnOff = turnOff;
/**
Expand All @@ -111,18 +135,35 @@ const toggle = (device) => {
}
};
exports.toggle = toggle;
const generateIsOnBytes = (device) => {
if (device.type === DeviceType.LitraBeamLX) {
return (0, utils_1.padRight)([0x11, 0xff, 0x06, 0x01], 20, 0x00);
}
else {
return (0, utils_1.padRight)([0x11, 0xff, 0x04, 0x01], 20, 0x00);
}
};
/**
* Gets the current power state of your Logitech Litra device
*
* @param {Device} device The device to get the current power state for
* @returns {boolean} Current power state where true = on and false = off
*/
const isOn = (device) => {
device.hid.write((0, utils_1.padRight)([0x11, 0xff, 0x04, 0x01], 20, 0x00));
const bytes = generateIsOnBytes(device);
device.hid.write(bytes);
const data = device.hid.readSync();
return data[4] === 1;
};
exports.isOn = isOn;
const generateSetTemperatureInKelvinBytes = (device, temperatureInKelvin) => {
if (device.type === DeviceType.LitraBeamLX) {
return (0, utils_1.padRight)([0x11, 0xff, 0x06, 0x9c, ...(0, utils_1.integerToBytes)(temperatureInKelvin)], 20, 0x00);
}
else {
return (0, utils_1.padRight)([0x11, 0xff, 0x04, 0x9c, ...(0, utils_1.integerToBytes)(temperatureInKelvin)], 20, 0x00);
}
};
/**
* Sets the temperature of your Logitech Litra device
*
Expand All @@ -143,24 +184,42 @@ const setTemperatureInKelvin = (device, temperatureInKelvin) => {
if (!allowedTemperatures.includes(temperatureInKelvin)) {
throw `Provided temperature must be a multiple of 100 between ${minimumTemperature} and ${maximumTemperature} for this device`;
}
device.hid.write((0, utils_1.padRight)([0x11, 0xff, 0x04, 0x9c, ...(0, utils_1.integerToBytes)(temperatureInKelvin)], 20, 0x00));
const bytes = generateSetTemperatureInKelvinBytes(device, temperatureInKelvin);
device.hid.write(bytes);
};
exports.setTemperatureInKelvin = setTemperatureInKelvin;
const generateGetTemperatureInKelvinBytes = (device) => {
if (device.type === DeviceType.LitraBeamLX) {
return (0, utils_1.padRight)([0x11, 0xff, 0x06, 0x81], 20, 0x00);
}
else {
return (0, utils_1.padRight)([0x11, 0xff, 0x04, 0x81], 20, 0x00);
}
};
/**
* Gets the temperature of your Logitech Litra device
*
* @param {Device} device The device to get the temperature for
* @returns {number} The current temperature in Kelvin
*/
const getTemperatureInKelvin = (device) => {
device.hid.write((0, utils_1.padRight)([0x11, 0xff, 0x04, 0x81], 20, 0x00));
const bytes = generateGetTemperatureInKelvinBytes(device);
device.hid.write(bytes);
const data = device.hid.readSync();
// data[4] is the multiple of 256
// data[5] is the remainder of 256
// together they come out to the temp in K
return data[4] * 256 + data[5];
};
exports.getTemperatureInKelvin = getTemperatureInKelvin;
const generateSetBrightnessInLumenBytes = (device, brightnessInLumen) => {
if (device.type === DeviceType.LitraBeamLX) {
return (0, utils_1.padRight)([0x11, 0xff, 0x06, 0x4c, ...(0, utils_1.integerToBytes)(brightnessInLumen)], 20, 0x00);
}
else {
return (0, utils_1.padRight)([0x11, 0xff, 0x04, 0x4c, ...(0, utils_1.integerToBytes)(brightnessInLumen)], 20, 0x00);
}
};
/**
* Sets the brightness of your Logitech Litra device, measured in Lumen
*
Expand All @@ -178,17 +237,27 @@ const setBrightnessInLumen = (device, brightnessInLumen) => {
if (brightnessInLumen < minimumBrightness || brightnessInLumen > maximumBrightness) {
throw `Provided brightness must be between ${minimumBrightness} and ${maximumBrightness} for this device`;
}
device.hid.write((0, utils_1.padRight)([0x11, 0xff, 0x04, 0x4c, ...(0, utils_1.integerToBytes)(brightnessInLumen)], 20, 0x00));
const bytes = generateSetBrightnessInLumenBytes(device, brightnessInLumen);
device.hid.write(bytes);
};
exports.setBrightnessInLumen = setBrightnessInLumen;
const generateGetBrightnessInLumenBytes = (device) => {
if (device.type === DeviceType.LitraBeamLX) {
return (0, utils_1.padRight)([0x11, 0xff, 0x06, 0x31], 20, 0x00);
}
else {
return (0, utils_1.padRight)([0x11, 0xff, 0x04, 0x31], 20, 0x00);
}
};
/**
* Gets the current brightness of your Logitech Litra device, measured in Lumen
*
* @param {Device} device The device to get the current brightness for
* @returns {number} The current brightness in Lumen
*/
const getBrightnessInLumen = (device) => {
device.hid.write((0, utils_1.padRight)([0x11, 0xff, 0x04, 0x31], 20, 0x00));
const bytes = generateGetBrightnessInLumenBytes(device);
device.hid.write(bytes);
const data = device.hid.readSync();
return data[5];
};
Expand Down Expand Up @@ -224,6 +293,8 @@ const getDeviceTypeByProductId = (productId) => {
case PRODUCT_IDS[1]:
case PRODUCT_IDS[2]:
return DeviceType.LitraBeam;
case PRODUCT_IDS[3]:
return DeviceType.LitraBeamLX;
default:
throw 'Unknown device type';
}
Expand Down
3 changes: 2 additions & 1 deletion dist/esm/driver.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/// <reference types="node" />
export declare enum DeviceType {
LitraGlow = "litra_glow",
LitraBeam = "litra_beam"
LitraBeam = "litra_beam",
LitraBeamLX = "litra_beam_lx"
}
export interface Device {
hid: {
Expand Down
Loading