node-usbrelay
v1.0.0
Published
API for controlling one or multiple Sainsmart 16ch USB Relay Board(s)
Downloads
11
Maintainers
Readme
usbrelay
Node Promise-based API for controlling one or multiple Sainsmart 16ch USB Relay Board(s)
Motivation
I had been using Sainsmart's 16ch GPIO Relay Boards with a Raspberry Pi Model 3 B+ in some automation projects I was working on. They work pretty well, but wiring is pretty annoying/messy and using multiple boards wasn't really possible as the Raspi only supports up to 26 GPIO devices.
So when I saw this Sainsmart 16ch USB Relay Board, I was pretty excited to try it out. The Raspi has 4 USB ports, which meant I could potentially control up to 64 devices with one Pi. The only problem was figuring out how to actually get them to work as Sainsmart's documentation is pretty non-existent. After a few frustrating days of little progress, I was able to get one working. I originally set up this repo explaining the setup process and providing a very simple interfacing script, but I have since developed this repo as a more traditional node module for you to include in your project. Hope this saves you some time and frustration.
Getting Started
Referencing my other repo I linked above, to get these Relay Boards to work with the Raspi, you have to download a ch341 driver. The steps for downloading and installing this driver are as follows:
sudo apt-get update
,sudo apt-get upgrade
- Download the driver -
sudo wget https://github.com/aperepel/raspberrypi-ch340-driver/releases/download/4.4.11-v7/ch34x.ko
- Update your pi -
sudo rpi-update
(optional) - Reboot your pi to implement changes -
sudo reboot
- Check to make sure ch341.ko is installed -
ls /lib/modules/$(uname -r)/kernel/drivers/usb/serial
($(uname -r) should evaluate to something like "4.14.58-v7+") - Plug the Relay Board into your Pi
- Check to make sure ch341 (and usbserial) process is running -
lsmod
- Check to make sure the Relay Board has been recognized through USB -
ls /dev/tty*
(Look for 'ttyUSB0')
If you're using multiple boards on the same Pi, I would suggest setting up udevadm rules for adding SYMLINKs to each so you can differentiate between them (the boards themselves don't typically have serial numbers and ttyUSB# links can change at any time, so it can be difficult to differentiate). We will be assigning SYMLINKs based on the physical USB port you plug the board into, so make sure you set up a consistent system for making sure each board is actually plugged into the correct port. To set up a udevadm rule for these boards, follow these steps:
- Unplug all boards from the Pi
- For board1, plug it into the port you want to SYMLINK to
- Run
dmesg | grep ttyUSB
- you should see something like "usb 1-1.3: ch341-uart converter now attached to ttyUSB0" - The part I highlighted in bold represents the port designation, mark this down
- Repeat 1-3 for each relay you want to connect to the Pi
- Once you have all the port designations, you're ready to write a udevadm rule -
sudo nano /etc/udev/rules.d/10-usb-serial.rules
(rules are assigned based on the leading number, so using '10' will make sure our rule gets applied before other usb rules) - For each board, add a new line (making sure you use the appropriate port designation for each KERNELS attribute and a unique name for each SYMLINK):
KERNEL=="ttyUSB*", KERNELS=="1-1.3", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", SYMLINK+="usbRelay1"
- Save your changes and implement the rule by running
sudo udevadm trigger
or rebootingsudo reboot
- To confirm, run
ls /dev/<SYMLINK name of relay here>
and you should see it has been assinged to a ttyUSB link
That's it. Your boards should now be accessible at /dev/<SYMLINK name of relay here>
which is what we'll be using in this module.
Installation
Ok, now that we have our Pi all set up, let's install this module:
$ npm install -save node-usbrelay
Usage
This module has two constructors: RelayBoard
and RelayGroup
RelayBoard
RelayBoard exposes methods for querying and altering the state of an individual relay board. So if you're only using a single board, require RelayBoard from the module and initialize it with the SYMLINK path to your board:
const { RelayBoard } = require('node-usbrelay');
var board1 = new RelayBoard({ port: '/dev/usbRelay1' });
The RelayBoard
constructor requires a port designation to initialize. You can also pass it a name
as well as a true/false flag as the test
property if you just want to play around with the methods without having a physical board connected.
A RelayBoard object has the following methods:
getState
The getState method returns an array representing the state of each relay with '0' representing NC (usually off) and '1' representing NO (usually on):
const { RelayBoard } = require('node-usbrelay');
var board1 = new RelayBoard({ port: '/dev/usbRelay1' });
console.log(board1.getState());
/* Example output
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
*/
setState
The setState method accepts a state array, which must contain 16 entries of either '0' or '1', and uses the toggle method to change the state of each relay as described by the array. It returns a Promise which resolves an object which includes an array of errors and the resulting state. The errors are included in the resolve rather than being rejected/thrown so you can decide how you would like to handle them (e.g. retry operation, undo previous action, quit entirely) without it automatically being thrown to the catch statement:
const { RelayBoard } = require('node-usbrelay');
var board1 = new RelayBoard({ port: '/dev/usbRelay1' });
board1.setState([ 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1 ])
.then(({ errors, state }) => {
if (errors.length) console.log(`Errors: ${errors}. Consider retrying or undoing previous action.`)
else console.log(state);
})
.catch(err => console.log(err));
/* Example output
[0,1,1,0,1,0,0,0,0,1,1,0,1,0,0,1]
*/
toggle & toggleOne
The toggle method accepts an array of relay numbers (1-16) and a command ("on" or "off"). It returns a promise which resolves an object with errors and the resulting state:
const { RelayBoard } = require('node-usbrelay');
var board1 = new RelayBoard({ port: '/dev/usbRelay1' });
board1.toggle([ 2, 3, 5, 10, 11, 13, 16 ], 'on')
.then(({ errors, state }) => {
console.log(`Toggle state: ${state}`);
if (errors.length) throw new Error(`Errors: ${errors}`);
return board1.toggleOne(8, 'on');
})
.then(state => console.log(`ToggleOne state: ${state}`))
.catch(err => console.log(err));
/* Example output
Toggle state: [0,1,1,0,1,0,0,0,0,1,1,0,1,0,0,1]
ToggleOne state: [0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1]
*/
reset
The reset method turns all relays "off". It is similar to using setState with a 16x'0' array, but much quicker:
const { RelayBoard } = require('node-usbrelay');
var board1 = new RelayBoard({ port: '/dev/usbRelay1' });
board1.toggle([ 2, 3, 5, 10, 11, 13, 16 ], 'on')
.then(({ errors, state }) => {
console.log(`Toggle state: ${state}`);
if (errors.length) console.log(`Errors: ${errors}`);
return board1.reset();
})
.then(state => console.log(`Reset state: ${state}`))
.catch(err => console.log(err));
/* Example output
Toggle state: [0,1,1,0,1,0,0,0,0,1,1,0,1,0,0,1]
Reset state: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
*/
RelayGroup
If you have multiple boards connected to your Pi, I suggest using the RelayGroup constructor, which allows you to manage all boards at once and also features a discovery method:
const { RelayGroup } = require('node-usbrelay');
var relayGroup = new RelayGroup();
If you already know the ports you want to initialize, you can pass a an array of RelayBoard property objects to the constructor:
const { RelayGroup } = require('node-usbrelay');
var ports = [
{ port: '/dev/usbRelay1', name: 'foo' },
{ port: '/dev/usbRelay2', name: 'bar' },
{ port: '/dev/usbRelay3', name: 'baz' }
];
var relayGroup = new RelayGroup({ ports });
A RelayGroup object shares a lot of the same methods as RelayBoard, with some additional capabilities:
listPorts & findBoards
The listPorts and findBoards methods both utilize the serialport library's list method to find available ports/boards. The listPorts method will list all available ports, whereas the findBoards method will filter the list to just find connected Sainsmart 16ch Relay Boards:
const { RelayGroup } = require('node-usbrelay');
var relayGroup = new RelayGroup();
relayGroup.findBoards()
.then(found => console.log(JSON.stringify(found, null, 4)))
.catch(err => console.log(err));
assignBoards
The assignBoards method accepts an array of board designations, initializing a new RelayBoard object for each. It will throw an error if the board initialization fails for any reason. Otherwise it returns the number of boards that were successfully initialized (note: this method is not necessary if you initialize the RelayGroup constructor with a ports array):
const { RelayGroup } = require('node-usbrelay');
var relayGroup = new RelayGroup();
var ports = [
{ port: '/dev/usbRelay1', name: 'foo' },
{ port: '/dev/usbRelay2', name: 'bar' },
{ port: '/dev/usbRelay3', name: 'baz' }
];
console.log(relayGroup.assignBoards(ports));
/* Example output
3
*/
getStates
The getStates method returns an array of the state of each initialized RelayBoard (note: the array is filled in the order in which the RelayBoards were initialized - meaning the first array corresponds to the state of '/dev/usbRelay1' and so on):
const { RelayGroup } = require('node-usbrelay');
var ports = [
{ port: '/dev/usbRelay1', name: 'foo' },
{ port: '/dev/usbRelay2', name: 'bar' },
{ port: '/dev/usbRelay3', name: 'baz' }
];
var relayGroup = new RelayGroup({ ports });
console.log(relayGroup.getStates());
/* Example output
[
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
]
*/
setStates
The setStates method can be used to specify desired states for each RelayBoard. It's best used in the case that you have multiple pre-defined configurations you want to swtich between. There are two ways to construct your input stateArray:
- Single Array
stateArray has a length of the number of initialized RelayBoards multiplied by 16. Relays are designated according to the order in which their RelayBoard was intialized, so '/dev/usbRelay1' has relays 1-16, '/dev/usbRelay2' has 17-32, and '/dev/usbRelay3' has 33-48. stateArray index position + 1 = Relay# (wasn't quite sure how else to say that clearly)
- Array of Arrays
Each array inside the main array represent the board at that index, so the first array represents the state of '/dev/usbRelay1' for example. You must include as many arrays as there are boards initialized or an error will be thrown
const { RelayGroup } = require('node-usbrelay');
var ports = [
{ port: '/dev/usbRelay1', name: 'foo' },
{ port: '/dev/usbRelay2', name: 'bar' },
{ port: '/dev/usbRelay3', name: 'baz' }
];
var relayGroup = new RelayGroup({ ports });
// turn on the first (local 1) and last (local 16) relay of each board
// using single Array
var singleArray = [
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1
]
relayGroup.setStates(singleArray, 'on')
.then(({ errors, states}) => {
errors.forEach((arr, i) => arr.length && console.log(`Errors on board ${i}: ${arr}`));
console.log(states);
})
.catch(err => console.log(err));
// using Array of Arrays
var arrOfArr = [
[ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 ],
[ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 ],
[ 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 ]
];
relayGroup.setStates(arrOfArr, 'on')
.then(({ errors, states}) => {
errors.forEach((arr, i) => arr.length && console.log(`Errors on board ${i}: ${arr}`));
console.log(states);
})
.catch(err => console.log(err));
/* Example output for both
[
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]
]
*/
toggle & toggleOne
The toggle and toggleOne methods are similar to the RelayBoard methods of the same name. The difference is that these methods can access each initialized RelayBoard in one command. Similarly to setStates, there are two ways to specify which relays on which RelayBoards you want to toggle:
- Single Array
Relays are accessible according to the order in which their RelayBoard was intialized, so '/dev/usbRelay1' has relays 1-16, '/dev/usbRelay2' has 17-32, and '/dev/usbRelay3' has 33-48. (note: the toggleOne method uses this numbering system exclusively)
- Array of Arrays
Each array inside the main array represent the board at that index, so the first array will toggle relays on '/dev/usbRelay1' for example. You must include as many arrays (even if they are emtpy) as there are boards initialized or an error will be thrown
const { RelayGroup } = require('node-usbrelay');
var ports = [
{ port: '/dev/usbRelay1', name: 'foo' },
{ port: '/dev/usbRelay2', name: 'bar' },
{ port: '/dev/usbRelay3', name: 'baz' }
];
var relayGroup = new RelayGroup({ ports });
// turn on the first (local 1) and last (local 16) relay of each board
// using single Array
relayGroup.toggle([ 1, 16, 17, 32, 33, 48 ], 'on')
.then(({ errors, states }) => {
if (errors.find(arr => arr.length)) throw new Error(errors);
console.log(states);
})
.catch(err => console.log(err));
// using Array of Arrays
relayGroup.toggle([[ 1, 16 ], [ 1, 16 ], [ 1, 16 ]], 'on')
.then(({ errors, states }) => {
if (errors.find(arr => arr.length)) throw new Error(errors);
console.log(states);
})
.catch(err => console.log(err));
/* Example output for both
[
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]
]
*/
reset
The reset method resets all connected RelayBoards to a 16x'0' state:
const { RelayGroup } = require('node-usbrelay');
var ports = [
{ port: '/dev/usbRelay1', name: 'foo' },
{ port: '/dev/usbRelay2', name: 'bar' },
{ port: '/dev/usbRelay3', name: 'baz' }
];
var relayGroup = new RelayGroup({ ports });
// turn on the first (local 1) and last (local 16) relay of each board and then reset them
relayGroup.toggle([ 1, 16, 17, 32, 33, 48 ], 'on')
.then(({ errors, states }) => {
if (errors.find(arr => arr.length)) throw new Error(errors);
console.log(states);
return relayGroup.reset();
})
.then(states => console.log(states))
.catch(err => console.log(err));
/* Example output
[
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]
]
]
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
]
*/
RelayGroup.boards
In addition to the top-level RelayGroup methods listed out above, the RelayGroup object exposes the RelayBoard methods of each initialized RelayBoard, so each RelayBoard is still able to be controlled individually. You can access individual boards through the boards array:
const { RelayGroup } = require('node-usbrelay');
var ports = [
{ port: '/dev/usbRelay1', name: 'foo' },
{ port: '/dev/usbRelay2', name: 'bar' },
{ port: '/dev/usbRelay3', name: 'baz' }
];
var relayGroup = new RelayGroup({ ports });
relayGroup.toggle([ 1, 16, 17, 32, 33, 48 ], 'on')
.then(({ errors, states }) => {
if (errors.find(arr => arr.length)) throw new Error(errors);
console.log(states);
return relayGroup.boards[1].reset();
})
.then(() => console.log(relayGroup.getStates()))
.catch(err => console.log(err));
/* Example output
[
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]
]
]
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]
]
*/
Updates
- ~~Get state of RelayBoard directly through a command rather than assuming 16x'0' state when initializing RelayBoards~~ (status command doesn't actually work : /)
- Add explicit support for 4,8ch relay boards
Contributing
I made this module on my own. Any help/feedback is appreciated.