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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for RGBW lights #2

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
58 changes: 40 additions & 18 deletions examples/movie_upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@ import { Light, Frame, Movie, Led } from "../dist/index.js";
async function run() {
// instantiate the device
console.log("Creating device...");
const addresses = ["192.168.4.1", "192.168.1.164"];
const device = new Light(addresses[1]);

let movie = makeMovie();
const device = new Light(addresses[0]);

// must login before sending commands
console.log("Logging in...");
await device.login();

const details = await device.getDeviceDetails();
const nLeds = await device.getNLeds();
// get the device name
console.log(`This device is called ${await device.getName()}`);

// Create a movie
let movie = makeMovie({
nLeds,
nFrames: nLeds + 20,
tailLength: 20,
type: details.led_profile?.toLowerCase() || 'rgb',
});
Comment on lines +18 to +23
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this function configurable so it'll match the device's specs

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh also nFrames = nLeds + tailLength lets the tail fade out rather than cutting it off


// adjust brightness
console.log("Set device to full brightness");
await device.setBrightness(100);
Expand Down Expand Up @@ -42,16 +51,21 @@ async function run() {

run();

function makeMovie() {
const nLeds = 600;
const nFrames = 600;
const tailLength = 15;
const black = new Led(0, 0, 0);
const fps = 15;
function makeMovie({
nLeds = 600,
nFrames = 250,
tailLength = 15,
type = "rgb",
fps = 15,
} = {}) {
const rgbw = type === "rgbw";
const black = rgbw
? new Led(0, 0, 0, 0)
: new Led(0, 0, 0);
const frames = [];
const saturationFactor = 0.5;
const nBufferFrames = 3 * fps;
const step = 3;
const step = 5; // skip frames to make movie shorter
const nBufferFrames = 3 * fps; // add some blank frames at the end

for (let i = 0; i < nFrames; i += step) {
// Faster way to make a frame of LEDs of single color
Expand All @@ -64,11 +78,12 @@ function makeMovie() {
if (j === 0) {
sparkle = 1;
}
if (i - j !== undefined) {
let r = 0;
let g = 0;
let b = 255;
leds[i - j] = new Led(r, g, b)
if (leds[i - j] !== undefined) {
const r = 0;
const g = 0;
const b = 255;
const w = rgbw ? 0 : undefined;
leds[i - j] = new Led(r, g, b, w)
// .desaturate(1)
.desaturate(desaturation)
// .brighten(sparkle)
Expand All @@ -84,7 +99,14 @@ function makeMovie() {
frames.push(new Frame(Array(nLeds).fill(black)));
}

let movie = new Movie({ frames, fps, name: "fairy_15fps" });
const duration = Math.round(frames.length / fps);

let movie = new Movie({
frames,
fps,
name: `fairy_${fps}fps_${duration}s`,
descriptor_type: type + '_raw'
});

return movie;
}
Expand Down
11 changes: 6 additions & 5 deletions src/lib/frame.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Led } from "./led.js";
import type { Led } from "./led";

/**
* A frame of LEDs, used when you wish to set color pixel by pixel
Expand Down Expand Up @@ -26,12 +26,13 @@ export class Frame {
* @returns {Uint8Array}
*/
toOctet(): Uint8Array {
let buffer = new ArrayBuffer(this.leds.length * 3);
let output = new Uint8Array(buffer);
const channels = this.leds[0].type.length || 3;
const buffer = new ArrayBuffer(this.leds.length * channels);
const output = new Uint8Array(buffer);
let offset = 0;
this.leds.forEach((led) => {
output.set(led.toOctet(), offset);
offset += 3;
output.set(led.toOctet(), offset);
offset += channels;
});
return output;
}
Expand Down
72 changes: 66 additions & 6 deletions src/lib/led.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export class Led {
red: number;
green: number;
blue: number;
white?: number;

private _type: 'rgb'|'rgbw';

/**
* Creates an instance of the Led class.
Expand All @@ -19,10 +22,51 @@ export class Led {
* @param {number} green - Green value (0-255).
* @param {number} blue - Blue value (0-255).
*/
constructor(red: number, green: number, blue: number) {
constructor(red: number, green: number, blue: number, white?: number) {
this.red = red;
this.green = green;
this.blue = blue;
this.white = white;
this._type = typeof white === 'number' ? 'rgbw' : 'rgb';
}

/**
* Gets the LED type.
* @returns {'rgb'|'rgbw'} The LED type.
*/
get type(): 'rgb' | 'rgbw' {
return this._type;
}

/**
* Converts the LED to RGBW.
*
* @returns {Led} The updated Led instance.
*/
toRgbw(): this {
if (this._type === 'rgbw') return this;
this.white = 0;
this._type = 'rgbw';
return this;
}

/**
* Converts the LED to RGB.
*
* @param {boolean} [preserveWhite] - If true, the white value will be preserved.
* @returns {Led} The updated Led instance.
*/
toRgb(preserveWhite = false): this {
const white = this.white;
if (this._type === 'rgb') return this;
this.white = undefined;
this._type = 'rgb';

if (white && preserveWhite) {
this.brighten(white / 255);
}
Comment on lines +65 to +67
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbf this is probably unnecessary


return this;
}

/**
Expand All @@ -31,7 +75,11 @@ export class Led {
* @returns {Uint8Array} The RGB values in a Uint8Array format.
*/
toOctet(): Uint8Array {
return new Uint8Array([this.red, this.green, this.blue]);
return new Uint8Array(
this._type === 'rgbw'
? [this.white!, this.red, this.green, this.blue]
: [this.red, this.green, this.blue]
);
Comment on lines +78 to +82
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently RGBW products use white as the first byte 🤷

}

/**
Expand All @@ -40,7 +88,7 @@ export class Led {
* @returns {boolean} True if the LED is on, false otherwise.
*/
isOn(): boolean {
return this.red > 0 || this.green > 0 || this.blue > 0;
return this.red > 0 || this.green > 0 || this.blue > 0 || this.white! > 0;
}

/**
Expand All @@ -52,6 +100,7 @@ export class Led {
this.red = 0;
this.green = 0;
this.blue = 0;
this._type === 'rgbw' && (this.white = 0);
return this;
}

Expand All @@ -63,10 +112,14 @@ export class Led {
* @param {number} blue - New blue value.
* @returns {Led} The updated Led instance.
*/
setColor(red: number, green: number, blue: number): this {
setColor(red: number, green: number, blue: number, white?: number): this {
this.red = red;
this.green = green;
this.blue = blue;
if (typeof white === 'number') {
this.white = white;
this._type = 'rgbw';
}
return this;
}

Expand All @@ -79,6 +132,7 @@ export class Led {
this.red = 255 - this.red;
this.green = 255 - this.green;
this.blue = 255 - this.blue;
typeof this.white === 'number' && (this.white = 255 - this.white);
return this;
}

Expand All @@ -88,7 +142,7 @@ export class Led {
* @returns {string} String in the format 'rgb(r, g, b)'.
*/
toString(): string {
return `rgb(${this.red}, ${this.green}, ${this.blue})`;
return `${this._type}(${this.red}, ${this.green}, ${this.blue}, ${this.white})`;
}

/**
Expand All @@ -101,6 +155,7 @@ export class Led {
this.red = Math.min(255, Math.round(this.red * factor));
this.green = Math.min(255, Math.round(this.green * factor));
this.blue = Math.min(255, Math.round(this.blue * factor));
this._type === 'rgbw' && (this.white = Math.min(255, Math.round((this.white || 0) * factor)));
return this;
}

Expand All @@ -114,6 +169,7 @@ export class Led {
this.red = Math.max(0, Math.round(this.red * factor));
this.green = Math.max(0, Math.round(this.green * factor));
this.blue = Math.max(0, Math.round(this.blue * factor));
this._type === 'rgbw' && (this.white = Math.max(0, Math.round((this.white || 0) * factor)));
return this;
}

Expand All @@ -137,6 +193,10 @@ export class Led {
255,
Math.max(0, average + factor * (this.blue - average))
);
this._type === 'rgbw' && (this.white = Math.min(
255,
Math.max(0, average + factor * ((this.white || 0) - average))
));
return this;
}

Expand All @@ -151,7 +211,7 @@ export class Led {
this.red = this.red + factor * (average - this.red);
this.green = this.green + factor * (average - this.green);
this.blue = this.blue + factor * (average - this.blue);
this._type === 'rgbw' && (this.white = (this.white || 0) + factor * (average - (this.white || 0)));
return this;
}
}

14 changes: 9 additions & 5 deletions src/lib/light.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { generateRandomHex } from "./utils.js";

import axios, { AxiosInstance, AxiosResponse } from "axios";
import FetchWrapper, { FetchResponse } from "./fetchwrapper.js";
import FetchWrapper, { FetchResponse } from "./fetchwrapper";
import delay from "delay";
// dynamically import udp for compatibility with browser
// import * as udp from "node:dgram";

import { Led } from "./led.js";
import { Frame } from "./frame.js";
import { Movie } from "./movie.js";
import { Led } from "./led";
import { Frame } from "./frame";
import { Movie } from "./movie";
Comment on lines +9 to +11
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the .js extension throughout fixed some module resolution quirks in VSCode for me


import {
rgbColor,
Expand All @@ -18,7 +18,7 @@ import {
timer,
coordinate,
layout,
} from "./interfaces.js";
} from "./interfaces";

// create error
let errNoToken = Error("No valid token");
Expand All @@ -35,6 +35,7 @@ export class Light {
token: AuthenticationToken | undefined;
activeLoginCall: boolean;
nleds: number | undefined;
name: string | undefined;
udpClient: any; //udp.Socket;
/**
* Creates an instance of Light.
Expand Down Expand Up @@ -239,6 +240,8 @@ export class Light {
*/
async getDeviceDetails(): Promise<object> {
let data = await this.sendGetRequest("/gestalt", undefined, false);
this.nleds ??= data.number_of_led;
this.name ??= data.device_name;
return data;
}
/**
Expand All @@ -264,6 +267,7 @@ export class Light {
* @returns {Promise<string>} Name of device
*/
async getName(): Promise<string> {
if (this.name) return this.name;
let data = await this.sendGetRequest("/device_name");
let res: string = data.name;
return res;
Expand Down
21 changes: 13 additions & 8 deletions src/lib/movie.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Frame } from "./frame.js";
import type { Frame } from "./frame";

export class Movie {
id: number;
Expand All @@ -10,7 +10,10 @@ export class Movie {
frames_number: number;
fps: number;
frameData: Frame[];
constructor(data: any) {

private _channels: number;

constructor(data: Record<string, any>) {
// Required for toOctet()
this.frameData = data.frames || null;

Expand All @@ -32,6 +35,8 @@ export class Movie {
this.frames_number = data.frames_number || this.frameData.length;
this.fps = data.fps || 0;

this._channels = this.descriptor_type.startsWith("rgbw") ? 4 : 3;

// Not used yet
this.id = data.id || 0;
this.loop_type = data.loop_type || 0;
Expand All @@ -53,7 +58,7 @@ export class Movie {
this.frames_number = frames.length;
this.leds_per_frame = frames[0].getNLeds();
const buffer = new ArrayBuffer(
this.frames_number * this.leds_per_frame * 3
this.frames_number * this.leds_per_frame * this._channels
);
const output = new Uint8Array(buffer);
frames.forEach((frame, index) => {
Expand All @@ -64,7 +69,7 @@ export class Movie {
let octet = frame.toOctet();

// add octet to output
let offset = index * this.leds_per_frame * 3;
let offset = index * this.leds_per_frame * this._channels;
output.set(octet, offset);
});
// this.frameData = output;
Expand All @@ -76,12 +81,12 @@ export class Movie {
this.frameData.forEach((frame) => {
let leds = frame.leds;
if (isCompressed) {
leds = frame.leds.filter((led) => {
return led.red != 0 && led.green != 0 && led.blue != 0;
});
leds = frame.leds.filter(
(led) => led.red || led.green || led.blue || led.white
);
}
let numNonBlackLeds = leds.length;
nBytes += numNonBlackLeds * 3;
nBytes += numNonBlackLeds * this._channels;
});
return nBytes;
}
Expand Down
Loading