Skip to content

Commit ca5c405

Browse files
committedJun 14, 2020
Added install script & systemd config + docs
1 parent 24170ed commit ca5c405

File tree

5 files changed

+170
-56
lines changed

5 files changed

+170
-56
lines changed
 

‎README.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,11 @@
1-
# lightr
1+
# Lightr
22
Control Hue lights in a room with a Raspberry Pi and roatry encoder switches
3+
4+
# Running
5+
- Copy/clone the source across to a Raspberry Pi running Raspbian
6+
- Run ```cp config.default.json config.json``` and then open config.json and ensure the Philips Hue bridge IP/UserId & GPIO pin configuration matches the GPIO connections you've set up
7+
- Run ```sudo scripts/install_deps.sh``` from the source root folder
8+
- Run ```npm install```
9+
- Run ``sudo systemctl restart lightr```
10+
11+
The app should now start up (and automatically restart when the Pi is rebooted). The first screen will give you an option to select which light group it should control (You can get back to this screen laster by pressing the toggle buttons in the order 3-2-1-2-3-1).

‎config.default.json

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
{
22
"bridgeIp": "192.168.0.4",
33
"userId": "xyzzy",
4-
"aPin": 17,
5-
"bPin": 18,
6-
"togglePin": 27
4+
"a1Pin": 17,
5+
"b1Pin": 27,
6+
"toggle1Pin": 22,
7+
"a2Pin": 9,
8+
"b2Pin": 11,
9+
"toggle2Pin": 10,
10+
"a3Pin": 6,
11+
"b3Pin": 13,
12+
"toggle3Pin": 5
713
}

‎index.js

+112-52
Original file line numberDiff line numberDiff line change
@@ -7,43 +7,63 @@ const storage = require('node-persist');
77
const {
88
bridgeIp,
99
userId,
10-
aPin, bPin, togglePin
10+
a1Pin, b1Pin, toggle1Pin,
11+
a2Pin, b2Pin, toggle2Pin,
12+
a3Pin, b3Pin, toggle3Pin
1113
} = require('./config.json');
1214

13-
const FIELDS = {'bri': 8, 'hue': 1024, 'sat': 8};
15+
const FIELDS = {'hue': 1024, 'sat': 8, 'bri': 8};
16+
const SELECT_PATTERN = [1,2,3,2,1,3];
1417

1518
class RotaryEncoder extends EventEmitter {
16-
constructor({a,b,toggle}) {
19+
constructor({
20+
a1,b1,toggle1,
21+
a2,b2,toggle2,
22+
a3,b3,toggle3,
23+
}) {
1724
super();
18-
this.gpioA = new Gpio(a, 'in', 'both');
19-
this.gpioB = new Gpio(b, 'in', 'both');
20-
this.gpioToggle = new Gpio(toggle, 'in', 'rising', { debounceTimeout: 10 });
21-
this.gpioA.watch((err, value) => {
25+
this.gpio1A = new Gpio(a1, 'in', 'both');
26+
this.gpio1B = new Gpio(b1, 'in', 'both');
27+
this.gpio1Toggle = new Gpio(toggle1, 'in', 'rising', { debounceTimeout: 10 });
28+
this.gpio2A = new Gpio(a2, 'in', 'both');
29+
this.gpio2B = new Gpio(b2, 'in', 'both');
30+
this.gpio2Toggle = new Gpio(toggle2, 'in', 'rising', { debounceTimeout: 10 });
31+
this.gpio3A = new Gpio(a3, 'in', 'both');
32+
this.gpio3B = new Gpio(b3, 'in', 'both');
33+
this.gpio3Toggle = new Gpio(toggle3, 'in', 'rising', { debounceTimeout: 10 });
34+
this._watch(1, this.gpio1A, this.gpio1B, this.gpio1Toggle);
35+
this._watch(2, this.gpio2A, this.gpio2B, this.gpio2Toggle);
36+
this._watch(3, this.gpio3A, this.gpio3B, this.gpio3Toggle);
37+
}
38+
39+
_watch(index, gpioA,gpioB,gpioToggle) {
40+
gpioA.watch((err, value) => {
2241
if (err) {
2342
this.emit('error', err);
2443
return;
2544
}
2645
const a = value;
2746

2847
try {
29-
const b = this.gpioB.readSync();
48+
const b = gpioB.readSync();
3049
if (a === b) {
31-
this.emit('rotation', 1);
50+
this.emit('rotation'+index, 1);
3251
} else {
33-
this.emit('rotation', -1);
52+
this.emit('rotation'+index, -1);
3453
}
3554
} catch (ex) {
3655
this.emit('error', ex);
3756
}
3857
});
39-
this.gpioToggle.watch((err, value) => {
58+
gpioToggle.watch((err, value) => {
4059
if (err) {
4160
this.emit('error', err);
4261
return;
4362
}
44-
this.emit('toggle');
63+
this.emit('toggle'+index);
4564
});
46-
}
65+
66+
}
4767
}
4868

4969
class HueAPI {
@@ -162,6 +182,7 @@ function throttlePromise(fn,{ reduce, debounce, delay } = { reduce: null, deboun
162182
completed();
163183
}
164184
}).catch((e) => {
185+
console.log(e.stack);
165186
running = false;
166187
if (!waiting) {
167188
completed();
@@ -203,7 +224,7 @@ class LightGroupController {
203224
this.api = api;
204225
this.worker = worker;
205226
this.groupState = null;
206-
this.fieldIndex = 0;
227+
this.matchIndex = 0;
207228
}
208229

209230
async init() {
@@ -215,41 +236,68 @@ class LightGroupController {
215236

216237
async onEvent(event, args) {
217238
switch (event) {
218-
case 'rotation': {
219-
const field = Object.keys(FIELDS)[this.fieldIndex];
220-
const change = {
221-
[field]: FIELDS[field] * args,
222-
'on': true
223-
};
224-
if (this.groupState) {
225-
Object.keys(change).forEach(k => {
226-
if (typeof change[k] === 'number') {
227-
this.groupState[k] += change[k]
228-
} else {
229-
this.groupState[k] = change[k]
230-
}
231-
});
232-
this.worker.send('lightgroup_control',this.groupState);
239+
case 'rotation1':
240+
await this._rotate('bri', args);
241+
break;
242+
case 'rotation2':
243+
await this._rotate('sat', args);
244+
break;
245+
case 'rotation3':
246+
await this._rotate('hue', args);
247+
break;
248+
249+
case 'toggle1':
250+
case 'toggle2':
251+
case 'toggle3': {
252+
if (this.matchIndex === SELECT_PATTERN.length) {
253+
return;
254+
}
255+
256+
// check if the user has entered the select screen pattern
257+
const index = parseInt(event[event.length - 1],10);
258+
if (index === SELECT_PATTERN[this.matchIndex++]) {
259+
if (this.matchIndex === SELECT_PATTERN.length) {
260+
const newController = new LightGroupSelector(this.api, this.worker);
261+
await newController.init();
262+
controller = newController;
263+
return;
264+
}
265+
} else {
266+
this.matchIndex = 0;
233267
}
234268

235-
const response = await this.api.putGroup(this.groupId, Object.keys(change).reduce((prev, next) => {
236-
const value = change[next];
237-
prev[next + (typeof value === 'number' ? '_inc' : '')] = value;
238-
return prev;
239-
}, {}));
269+
const response = await this.api.putGroup(this.groupId, {
270+
'on': this.groupState ? !this.groupState.on : true
271+
});
240272
await this.init();
241273
break;
242274
}
275+
}
276+
}
243277

244-
case 'toggle': {
245-
this.fieldIndex++;
246-
if (this.fieldIndex >= Object.keys(FIELDS).length) {
247-
this.fieldIndex = 0;
278+
async _rotate(field, value) {
279+
const change = {
280+
[field]: FIELDS[field] * value,
281+
'on': true
282+
};
283+
if (this.groupState) {
284+
Object.keys(change).forEach(k => {
285+
if (typeof change[k] === 'number') {
286+
this.groupState[k] += change[k]
287+
} else {
288+
this.groupState[k] = change[k]
248289
}
249-
break;
250-
}
290+
});
291+
this.worker.send('lightgroup_control',this.groupState);
251292
}
252-
}
293+
294+
const response = await this.api.putGroup(this.groupId, Object.keys(change).reduce((prev, next) => {
295+
const value = change[next];
296+
prev[next + (typeof value === 'number' ? '_inc' : '')] = value;
297+
return prev;
298+
}, {}));
299+
await this.init();
300+
}
253301
}
254302

255303
class LightGroupSelector {
@@ -270,7 +318,9 @@ class LightGroupSelector {
270318

271319
async onEvent(event, args) {
272320
switch (event) {
273-
case 'rotation': {
321+
case 'rotation1':
322+
case 'rotation2':
323+
case 'rotation3': {
274324
this.selected += (args > 0 ? 1: -1);
275325
if (this.selected >= this.options.length) {
276326
this.selected = this.options.length - 1;
@@ -284,7 +334,9 @@ class LightGroupSelector {
284334
break;
285335
}
286336

287-
case 'toggle': {
337+
case 'toggle1':
338+
case 'toggle2':
339+
case 'toggle3': {
288340
const groupId = this.options[this.selected].key;
289341
console.log('Selected groupId: ' + groupId);
290342
await storage.setItem('groupId', groupId);
@@ -303,9 +355,15 @@ storage.init().then(async () => {
303355
const api = new HueAPI(bridgeIp, userId);
304356
const uiWorker = new Worker();
305357
const encoder = new RotaryEncoder({
306-
a: aPin,
307-
b: bPin,
308-
toggle: togglePin
358+
a1: a1Pin,
359+
b1: b1Pin,
360+
toggle1: toggle1Pin,
361+
a2: a2Pin,
362+
b2: b2Pin,
363+
toggle2: toggle2Pin,
364+
a3: a3Pin,
365+
b3: b3Pin,
366+
toggle3: toggle3Pin
309367
});
310368

311369
const groupId = await storage.getItem('groupId');
@@ -317,11 +375,13 @@ storage.init().then(async () => {
317375
}
318376
await controller.init();
319377

320-
encoder.on('rotation', throttlePromise((value) => controller.onEvent('rotation', value), {
321-
debounce: 100,
322-
delay: 500,
323-
reduce: (prev, next) => [prev[0] + next[0]]
324-
}));
378+
for (let i = 1;i <= 3;++i) {
379+
encoder.on('rotation'+i, throttlePromise((value) => controller.onEvent('rotation'+i, value), {
380+
debounce: 100,
381+
delay: 500,
382+
reduce: (prev, next) => [prev[0] + next[0]]
383+
}));
384+
encoder.on('toggle'+i,() => controller.onEvent('toggle'+i));
385+
}
325386

326-
encoder.on('toggle',() => controller.onEvent('toggle'));
327387
});

‎scripts/install_deps.sh

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/bin/bash
2+
if [ "$(whoami)" != "root" ]; then
3+
echo "Sorry, you are not root. Re-run this script using sudo"
4+
exit 1
5+
fi
6+
7+
# install dependencies
8+
apt-get install i2c-tools
9+
10+
armVersion=$(uname -a | grep armv6l)
11+
12+
if [ "$armVersion" ]; then
13+
# install node for armv61
14+
wget https://nodejs.org/dist/v11.15.0/node-v11.15.0-linux-armv6l.tar.gz
15+
tar -xzf node-v11.15.0-linux-armv6l.tar.gz
16+
cp -R node-v11.15.0-linux-armv6l/* /usr/local/
17+
rm -rf node-v*
18+
else
19+
# for arm 7 we can just pull the latest version from apt
20+
apt-get install node npm
21+
fi
22+
23+
# set the app-server to auto start on boot
24+
cp scripts/systemd.conf /etc/systemd/system/lightr.service
25+
cwd=$(pwd)
26+
sed -i.bak 's|CWD|'"$cwd"'|g' /etc/systemd/system/lightr.service
27+
rm /etc/systemd/system/lightr.service.bak
28+
systemctl enable lightr

‎scripts/systemd.conf

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[Service]
2+
ExecStart=/usr/local/bin/node index.js
3+
Restart=always
4+
StandardOutput=syslog
5+
StandardError=syslog
6+
SyslogIdentifier=lightr
7+
Environment=NODE_ENV=production
8+
WorkingDirectory=CWD
9+
10+
[Install]
11+
WantedBy=multi-user.target

0 commit comments

Comments
 (0)