clexi
Version:
Node.js CLEXI is a lightweight client extension interface that enhances connected clients with functions of the underlying operating system using a duplex, realtime Websocket connection.
287 lines (265 loc) • 9.01 kB
JavaScript
//might be required: sudo usermod -a -G spi $USER
//double-check: ls -l /dev/spi*
//if you have connection issues: activate SPI interface in OS
const Color = require('color');
const spi = require('spi-device');
/*
Required interface functions:
- description(): Info about the item options and commands
- Class:
- constructor(options)
- init(successCallback, errorCallback)
- writeData(data, successCallback, errorCallback)
- readData(options, successCallback, errorCallback)
- release(successCallback, errorCallback)
- test(options): Function to test item (init, write, read, release)
*/
function description(){
return {
type: "ledArray",
options: [
{name: "numOfLeds", type: "number", min: 1},
{name: "ledType", type: "string", values: ["ws281x", "apa102"]},
{name: "spiBus", type: "number", values: [0, 1]},
{name: "spiDevice", type: "number", values: [0, 1]}
],
writeInterface: [
{name: "ledIndex", type: "number", min: 1},
{name: "red", type: "number", min: 0, max: 255},
{name: "green", type: "number", min: 0, max: 255},
{name: "blue", type: "number", min: 0, max: 255}
],
info: "Interface to control RGB LEDs via SPI. Supported types WS281X and APA102."
};
}
class GpioItem {
//WS281X and APA102 LEDs
//Reference: https://github.com/jgarff/rpi_ws281x
// https://github.com/respeaker/mic_hat/blob/master/interfaces/apa102.py
//Useful: https://github.com/zhincore/node-ws281x-spi
// https://github.com/joosteto/ws2812-spi
// https://github.com/jonnypage-d3/hooloovoo/blob/master/hooloovoo.js
constructor(options){
if (!options) options = {};
this.isReady = false;
//LED config
this._numOfLeds = options.numOfLeds || 1;
this._ledType = (options.ledType || "ws281x").toLowerCase();
//SPI bus/device
this._spiBusAndDevice = [options.spiBus || 0, options.spiDevice || 0];
//Type specific settings
this.spiInterface = undefined;
this.typeSupported = false;
if (this._ledType == "ws281x"){
//WS281X
this.typeSupported = true;
this._spiMaxSpeed = 4760000; //4.76Mhz core speed (we choose this just to get a multiple (2) of target)
this._ledBytes = this._numOfLeds * 9; //8 bits per RGB * 3 for pulse shape (100 = 0, 110 = 1)
this._spiOptions = {
maxSpeedHz: this._spiMaxSpeed
}
this._spiMsgOptions = {
//microSecondDelay: 0
speedHz: 2380000 //2.38Mhz target speed (core/2) for 420ns per bit
}
}else if (this._ledType == "apa102"){
//APA102
this.typeSupported = true;
this._spiMaxSpeed = 4000000; //4Mhz
this._ledBytes = this._numOfLeds * 4 + 8; //BGRb + 4 start frame bytes + 4 end frame bytes
this._spiOptions = {
maxSpeedHz: this._spiMaxSpeed
}
this._spiMsgOptions = {
speedHz: this._spiMaxSpeed
}
}else{
this.typeSupported = false;
this._ledBytes = 0;
}
//init. LED buffer
this._ledBuffer = Buffer.alloc(this._ledBytes); //full LEDs configuration
//set LED buffer
this.setLedBuffer = function(ledIndex, rgbRed, rgbGreen, rgbBlue){
//WS281X
if (this._ledType == "ws281x"){
//GRB colors
let currentLed = ledIndex * 9;
let colors = [Math.min(248, rgbGreen), Math.min(248, rgbRed), Math.min(248, rgbBlue)]; //RGB to GRB array - limit to 248 (255 is weird color shift)
//convert color data to ws281x compatible pulses (see specs) so that each bit is 420ns
let pwmBits = colors.map(function(color){
//convert each color component to binary octet and map to ws281x signal
let cBits = color.toString(2).padStart(8, 0); //color as 8 bits string
return Array.from(cBits).map(function(bit){
//embed each bit into 1x0, result: (1 0 0) => 0, (1 1 0) => 1 |-|__ and |--|_
return ("1" + bit + "0");
}).join("");
}).join("");
//binary wave for this index
let waveBinary = pwmBits.split("");
//convert 8-bits to 1-byte each to get our buffer hex values
let n = 0;
while (waveBinary.length){
let octet = waveBinary.splice(0, 8).join(""); //next 8 bits
let num = parseInt(octet, 2);
//console.log("ledIndex: " + ledIndex + ", n: " + n + ", value: " + num); //DEBUG
this._ledBuffer[currentLed + n] = num;
n++;
}
//APA102
}else if (this._ledType == "apa102"){
//BGRb colors
let brightness = 255; //NOTE: we keep this constant because it seems to be quirky and we have RGB
let currentLed = 4 + (ledIndex * 4);
this._ledBuffer[currentLed + 1] = rgbBlue;
this._ledBuffer[currentLed + 2] = rgbGreen;
this._ledBuffer[currentLed + 3] = rgbRed;
this._ledBuffer[currentLed + 0] = brightness;
}
//console.log("ledBuffer", this._ledBuffer); //DEBUG
}
//set LED buffer - HEX string format, e.g. "#00ff00"
this.setLedBufferHex = function(ledIndex, hexColorString){
let rgb = Color(hexColorString).rgb().array();
this.setLedBuffer(ledIndex, rgb[0], rgb[1], rgb[2]);
}
//set buffer for all LEDs
this.setBufferAll = function(red, green, blue){
for (let i=0; i < this._numOfLeds; i++) {
this.setLedBuffer(i, red, green, blue);
}
}
//transfer data
this.transferBuffer = function(successCallback, errorCallback){
let message = {};
Object.assign(message, this._spiMsgOptions, {
sendBuffer: this._ledBuffer,
byteLength: this._ledBuffer.length
});
this.spiInterface.transfer([message], (err, response) => {
if (err){
errorCallback(err);
}else{
//console.log("transfer response", response); //DEBUG
//TODO: use response?
successCallback({status: "transfered"});
}
});
}
}
init(successCallback, errorCallback){
if (!this.typeSupported){
errorCallback({name: "NotSupported", message: "'ledType' not supported (yet)"});
return;
}
//bus X, device Y
this.spiInterface = spi.open(this._spiBusAndDevice[0], this._spiBusAndDevice[1],
this._spiOptions, err => {
if (err){
errorCallback(err);
}else{
//set all off
this.setBufferAll(0, 0, 0);
var that = this;
this.transferBuffer(function(){
//done
that.isReady = true;
successCallback();
}, errorCallback);
}
});
}
writeData(data, successCallback, errorCallback){
if (!this.isReady){
errorCallback({name: "NotReady", message: "Interface not yet ready"});
}else if (!data
|| (!data.hex && (data.red == undefined || data.blue == undefined || data.green == undefined))
|| data.ledIndex == undefined
){
errorCallback({name: "MissingData", message: "Required: 'ledIndex' and {'red', 'green', 'blue'} or 'hex'"});
}else if (data.ledIndex < 1 || data.ledIndex > this._numOfLeds){
errorCallback({name: "WrongLedIndex", message: ("LED index must be between 1 and " + this._numOfLeds)});
}else{
try {
let internalIndex = data.ledIndex - 1;
if (data.hex){
this.setLedBufferHex(internalIndex, data.hex);
}else{
this.setLedBuffer(internalIndex, +data.red, +data.green, +data.blue);
}
//write
this.transferBuffer(successCallback, errorCallback);
}catch (err){
errorCallback(err);
}
}
}
readData(options, successCallback, errorCallback){
//TODO: converted to RGB data? read fresh from device?
if (this._ledBuffer){
successCallback({result: this._ledBuffer});
}else{
errorCallback({name: "NoData", message: "No data found"});
}
}
release(successCallback, errorCallback){
if (this.spiInterface){
var that = this;
//switch all off
this.setBufferAll(0, 0, 0);
this.transferBuffer(function(){
//close controller
that.spiInterface.close(function(err){
if (err){
errorCallback(err);
}else{
successCallback();
}
});
}, function(err){
errorCallback(err);
});
}else{
successCallback();
}
}
}
//console test call example: node -e 'require("./rpi-spi-rgb-leds").test({init: {numOfLeds: 1, ledType: "ws281x"}})'
function test(options){
if (!options) options = {};
var desc = description();
console.log("GPIO Item Test: " + desc.info);
var gpioItem = new GpioItem(options.init || {
numOfLeds: 1
});
console.log("GPIO init");
gpioItem.init(function(){
console.log("GPIO init: success - NEXT: write");
gpioItem.writeData(options.write || {
ledIndex: 1, red: 150, green: 0, blue: 150
}, function(){
console.log("GPIO write: success - NEXT: read in 3s");
setTimeout(function(){
gpioItem.readData(options.read || {}, function(data){
console.log("GPIO read: success - data:", data, "- NEXT: release");
gpioItem.release(function(data){
console.log("GPIO release: success - Item test: DONE");
}, function(err){
console.error("GPIO release: error", err);
});
}, function(err){
console.error("GPIO read: error", err);
});
}, 3000);
}, function(err){
console.error("GPIO write: error", err);
});
}, function(err){
console.error("GPIO init: error", err);
});
}
module.exports = {
description: description,
GpioItem: GpioItem,
test: test
};