Skip to content

Commit

Permalink
Merge pull request #14 from bytemind-de/dev
Browse files Browse the repository at this point in the history
added USB support for GPIO items + tests + demo file
  • Loading branch information
fquirin authored Jan 29, 2022
2 parents 8bd4fb0 + 5fadc0c commit ece7e8a
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 8 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog for CLEXI - Client Extension Interface

## v0.9.1
* Added package 'usb' to interface with USB devices
* Added custom gpio-item example 'respeaker-usb-array-v2' to control the LED array of a ReSpeaker Mic Array v2.0 via USB
* Added 'test(options)' to GPIO interface, e.g. for local testing

## v0.9.0
* Switched to Node.js 14 as officially supported version and changed repository for 'node-beacon-scanner' to more up-to-date version
* Added new extension 'gpio-interface' to communicate with the GPIO pins of Raspberry Pi (and similar boards)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Now you can run your server :-)
sudo node server.js
```
You should see a confirmation that the server is running and that extensions have been loaded (and hopefully no error ^^).
The `sudo` command is required for Bluetooth control. If you want to run the server without sudo you have to grant the node binary cap_net_raw privileges:
The `sudo` command is required for Bluetooth control. If you want to run the server without sudo you have to grant node cap_net_raw privileges:
```
sudo setcap cap_net_raw+eip $(eval readlink -f `which node`)
```
Expand Down
270 changes: 270 additions & 0 deletions gpio_items/respeaker-usb-array-v2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
//might be required (change group 'gpio' as you like):
//- sudo su -c "echo 'SUBSYSTEMS==\"usb\", ATTRS{idVendor}==\"2886\", ATTRS{idProduct}==\"0018\", GROUP=\"gpio\", MODE=\"0666\"' > /etc/udev/rules.d/99-ReSpeakerUSB.rules"
//- sudo usermod -a -G gpio $USER
//- reboot or replug USB device
const USB = require('usb');
//const BMRT = require('bmrequesttype');

/*
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: "ledArrayUsb",
options: [
{name: "mode", type: "string", values: ["sepia", "seeed"]},
{name: "brightness", type: "number", min: 1, max: 31}
],
writeInterface: [
{name: "state", type: "string", values: [
"idle", "loading", "listening", "speaking", "awaitDialog", "wakeWordActive", "wakeWordInactive"]},
{name: "rgb0Array", type: "array"}
],
info: "ReSpeaker Mic Array v2.0 USB LED interface."
};
}

class PixelRing {
constructor(device, mode) {
this.device = device;
this.mode = mode;
this.state = "";
this.vadState = "";

//init. LED buffer
this._numOfLeds = 12;
this._ledBits = this._numOfLeds * 4; //rgb0
this.ledArray = new Array(this._ledBits); //full LEDs configuration
this.ledArray.fill(0);

//set LED array
this.setLedArray = function(ledIndex, rgbRed, rgbGreen, rgbBlue, brightness){
let currentLed = (ledIndex - 1) * 4;
this.ledArray[currentLed + 0] = rgbRed;
this.ledArray[currentLed + 1] = rgbGreen;
this.ledArray[currentLed + 2] = rgbBlue;
this.ledArray[currentLed + 3] = brightness;
}
//set array for all LEDs
this.setLedArrayAll = function(red, green, blue, brightness){
for (let i=0; i < this._numOfLeds; i++) {
this.setLedArray(i, red, green, blue, brightness);
}
}
}

get availableStates() {
return ["idle", "loading", "listening", "speaking", "awaitDialog", "wakeWordActive", "wakeWordInactive", "custom"];
}
get currentState() {
return {state: this.state, vad: this.vadState};
}

write(cmd, data, stateInfo) {
if (data == undefined) data = [0];
const buffer = Buffer.from(data);
//var bmRt = BMRT.bmRequestType(BMRT.DIRECTION.Out, BMRT.TYPE.Vendor, BMRT.RECIPIENT.Device);
var bmRt = 0x40;
var that = this;
return new Promise(function(resolve, reject){
that.device.controlTransfer(bmRt, 0, cmd, 0x1C, buffer, function(err, data){
if (err){
reject(err);
}else{
if (stateInfo) that.state = stateInfo;
resolve(data);
}
});
});
}

//settings
traceMode() {
return this.write(0, [0], "trace");
}
vadLedMode(i) {
this.vadState = i; //TODO: unchecked state
return this.write(0x22, [i]); //0 - off, 1 - on, 3 - auto
}
brightness(i) {
return this.write(0x20, [i]); //range: 0x00 to 0x1F - or 0 to 31
}

//states
custom(rgb0Array, stateInfo) {
return this.write(6, rgb0Array, stateInfo); //TODO
}
idle() {
return this.write(1, [0, 0, 0, 0], "idle");
}
loading() {
return this.write(5, [0], "loading");
}
listening() {
return this.write(2, [0], "listening");
}
speaking() {
return this.write(4, [0], "speaking");
}
awaitDialog() {
this.setLedArray(1, 120, 120, 0, 0);
return this.custom(this.ledArray, "awaitDialog"); //TODO: test
}
wakeWordActive() {
return this.vadLedMode(1); //TODO: test
}
wakeWordInactive() {
return this.vadLedMode(0); //TODO: test
}
}

class GpioItem {
//ReSpeaker Mic Array v2.0 vendor specific USB LED interface.
//Reference 1: https://github.com/respeaker/pixel_ring/blob/master/pixel_ring/usb_pixel_ring_v2.py
//Reference 2: https://wiki.seeedstudio.com/ReSpeaker_Mic_Array_v2.0/#control-the-leds
//Node USB API: https://github.com/node-usb/node-usb#legacy-api

constructor(options){
if (!options) options = {};

this.isReady = false;

//USB Device
this._device = USB.findByIds(0x2886, 0x0018); //hardcoded vendor/product ID
this._device.timeout = 8000;
this.pixelRing;

//settings
this._mode = options.mode || "sepia"; //TODO: implement
this._brightness = options.brightness || 10;
}

init(successCallback, errorCallback){
if (!this._device){
errorCallback({name: "MissingDevice", message: "USB device not found"});
}else{
this.pixelRing = new PixelRing(this._device, this._mode);
var that = this;
try {
//open device
this._device.open();
//set inital modes (VAD LED off, brightness half)
that.pixelRing.vadLedMode(0).then(function(){
that.pixelRing.brightness(that._brightness)
}).then(function(){
//done
that.isReady = true;
successCallback();
}).catch(function(err){
errorCallback(err);
});
}catch (err){
errorCallback(err);
}
}
}

writeData(data, successCallback, errorCallback){
if (!this.isReady){
errorCallback({name: "NotReady", message: "Interface not yet ready"});
}else if (!data || !data.state){
errorCallback({name: "MissingData", message: "Required: state"});
}else if (this.pixelRing.availableStates.indexOf(data.state) < 0){
errorCallback({name: "WrongData", message: "Unknown state"});
}else{
try {
//write state
this.pixelRing[data.state](data.rgb0Array).then(function(res){
successCallback();
}).catch(function(err){
errorCallback(err);
});
}catch (err){
errorCallback(err);
}
}
}

readData(options, successCallback, errorCallback){
if (this.pixelRing){
var res = this.pixelRing.currentState;
successCallback({result: res});
}else{
errorCallback({name: "NoData", message: "No data found"});
}
}

release(successCallback, errorCallback){
if (this._device){
//switch all off
if (this.pixelRing){
//write state
var that = this;
this.pixelRing.idle().then(function(res){
//done
that._device.close();
successCallback();
}).catch(function(err){
//done
that._device.close();
errorCallback(err);
});
}else{
this._device.close();
successCallback();
}
}else{
successCallback();
}
}
}

//console test call example: node -e 'require("./respeaker-usb-array-v2").test({init: {}, write: {state: "speaking"}})'
function test(options){
if (!options) options = {};
var desc = description();
console.log("GPIO Item Test: " + desc.info);
var gpioItem = new GpioItem(options.init || {
mode: "sepia"
});
console.log("GPIO init");
gpioItem.init(function(){
console.log("GPIO init: success - NEXT: write");
gpioItem.writeData(options.write || {
state: "loading"
}, 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,
};
45 changes: 41 additions & 4 deletions gpio_items/rpi-respeaker-mic-hat-leds.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Required interface functions:
- writeData(data, successCallback, errorCallback)
- readData(options, successCallback, errorCallback)
- release(successCallback, errorCallback)
- test(options): Function to test item (init, write, read, release)
*/

function description(){
Expand All @@ -27,12 +28,12 @@ function description(){
{name: "green", type: "number", min: 0, max: 255},
{name: "blue", type: "number", min: 0, max: 255}
],
info: "ReSpeaker-ish audio HAT LED control via APA102 IC."
info: "ReSpeaker-ish audio HAT APA102 LEDs interface."
};
}

class GpioItem {
//APA102 IC LEDs - ReSpeaker MIC HAT
//APA102 LEDs - ReSpeaker MIC HAT
//Reference: https://github.com/respeaker/mic_hat/blob/master/interfaces/apa102.py
//Useful: https://github.com/jonnypage-d3/hooloovoo/blob/master/hooloovoo.js
// https://github.com/respeaker/4mics_hat/issues/2#issuecomment-471563162
Expand Down Expand Up @@ -77,7 +78,7 @@ class GpioItem {

//set LED buffer
this.setLedBuffer = function(ledIndex, rgbRed, rgbGreen, rgbBlue, brightness){
let currentLed = 4 + (ledIndex * 4)
let currentLed = 4 + (ledIndex * 4);
this._ledBuffer[currentLed + 1] = rgbBlue;
this._ledBuffer[currentLed + 2] = rgbGreen;
this._ledBuffer[currentLed + 3] = rgbRed;
Expand Down Expand Up @@ -209,7 +210,43 @@ class GpioItem {
}
}

//console test call example: node -e 'require("./rpi-respeaker-mic-hat-leds").test({init: {model: "2mic"}})'
function test(options){
if (!options) options = {};
var desc = description();
console.log("GPIO Item Test: " + desc.info);
var gpioItem = new GpioItem(options.init || {
model: "", 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
GpioItem: GpioItem,
test: test
};
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "clexi",
"version": "0.9.0",
"version": "0.9.1",
"description": "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.",
"main": "server.js",
"scripts": {
Expand Down Expand Up @@ -35,6 +35,7 @@
"fastify-static": "^2.7.0",
"fastify-ws": "^1.0.3",
"onoff": "^6.0.3",
"spi-device": "^3.1.2"
"spi-device": "^3.1.2",
"usb": "^2.0.3"
}
}
2 changes: 1 addition & 1 deletion server.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const fastify_static = require('fastify-static');
const fastify_ws = require('fastify-ws');

//Server
const version = "0.9.0";
const version = "0.9.1";
const settings = require('./settings.json');

var Clexi = function(customSettings){
Expand Down

0 comments on commit ece7e8a

Please sign in to comment.