/
blink1Service.js
500 lines (462 loc) · 15.6 KB
/
blink1Service.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
/**
*
*/
"use strict";
var _ = require('lodash');
var Blink1 = require('node-blink1');
var tinycolor = require('tinycolor2');
var config = require('../configuration');
var log = require('../logger');
var util = require('../utils');
var Eventer = require('../eventer');
// some globals because we are a singleton
/**
* Maximum support blink(1) devices for this service. GUI supports up to 8 I think
*/
var maxBlink1s = 4;
/**
* Number of individual LEDs allowed in a blink(1) device. Max is 18, 2 for dev
*/
var maxLEDsPerBlink1 = 2; // 18
var listeners = {}; // callback listeners
/**
* blink1 devices currently opened.
* One entry per blink1, with always at least one entry.
* Entry 0 is 'default' entry when no blink1 is plugged in.
* Format:
* { serial: '12345678',
* device: new Blink1()
* }
* @type {Array}
*/
var blink1s = []; // collection of opened blink1s
var currentBlink1Id = ''; // current blink1 (as set by colorpicker)
/**
* Current color state of all blink1s and their last used millis & ledns,
* with at least one entry.
* Entry 0 is 'default' entry when no blink1 is present.
* Supports 8 blink1s currently.
* @type Array
*/
var currentState = [];
for( var i=0; i< maxBlink1s; i++ ) {
var cs = new Array( maxLEDsPerBlink1);
cs.fill( tinycolor('#10100'+i));
currentState.push( {
colors: cs,
millis: 100,
ledn:0
}
);
}
var Blink1Service = {
// toyEnable: false,
// toyTimer:null,
// toyMode:'off',
// toyValue:0,
// settings for toy button commands
toy: {
enable: false,
timer: null,
interval: 0,
mode: 'off',
value: 0,
},
conf: {},
start: function() {
listeners = {}; // erase previous listeners
this.reloadConfig();
},
reloadConfig: function() {
log.msg("Blink1Service.reloadConfig");
this.conf = config.readSettings('blink1Service') || {};
Blink1Service._removeAllDevices();
Blink1Service.scanForDevices();
},
scanForDevices: function() {
// log.msg("Blink1Service.scanForDevices");
// initial population of any already-plugged in devices
var serials = Blink1.devices();
serials.sort();
serials.reverse(); // newest blink1s first
serials.map( function(s) {
Blink1Service._addDevice(s);
});
// log.msg("Blink1Service.scanForDevices: done. serials:", serials);
if( serials.length === 0 ) { // no blink1s, look for insertion events
if( this.conf.deviceRescan ) {
setTimeout( this.scanForDevices.bind(this), 5000); // look again in 5 secs
}
}
},
/**
* Add a blink1 to the device list.
* @constructor
* @param {string} serialnumber - serialnumber of blink1 to add
*/
_addDevice: function(serialnumber) {
log.msg("Blink1Service._addDevice:", serialnumber);
var olddev = _.find( blink1s, {serial:serialnumber} );
if( !olddev ) {
log.msg("Blink1Service._addDevice: new serial ", serialnumber);
blink1s.push( { serial: serialnumber, device: null, toSetup:true } );
}
// set up all devices at once
// we wait 500msec because it was failing without it
setTimeout( Blink1Service._setupFoundDevices, 500); // FIXME: an issue?
},
_setupFoundDevices: function() {
log.msg("Blink1Service._setupFoundDevices", blink1s);
blink1s.map( function(b1) {
if( !b1.device ) {
log.msg("Blink1Service._setupFoundDevice: opening ",b1.serial);
b1.device = new Blink1(b1.serial);
}
});
Eventer.emit('deviceUpdated');
Blink1Service.notifyChange();
},
/**
* Remove a blink1 from the device list. Triggers a scanForDevices()
* @method function
* @param {string} serialnumber serial number of blink1 to remove
*/
_removeDevice: function(serialnumber) {
log.msg("Blink1Service._removeDevice: current devices:", blink1s);
var olds = _.remove( blink1s, {serial:serialnumber} );
olds.forEach( function(b1) {
if( b1.device ) {
b1.device.close();
b1.device = null;
}
});
// if( currentBlink1Id === serialnumber ) {
// log.msg("FORGETTING OLD BLINK1!!!!");
// currentBlink1Id = 0;
// }
setTimeout( this.scanForDevices.bind(this), 5000);
},
_removeAllDevices: function() {
log.msg("Blink1Service._removeAllDevices");
blink1s.forEach( function(b1) {
if( b1.device ) {
b1.device.close();
b1.device = null;
}
});
blink1s = [ ]; //{serial: '', device: null } ]; // startup state
log.msg("Blink1Service._removeAllDevices: done");
// FIXME: notify? nah, because scanForDevices always called after
},
/**
* Fade to an RGB color over time, on particular LED and blink1 device.
* Private function, accesses hardware.
* assumes good & defined blink1idx, ledn, r,g,b, millis
* blink1idx is index into blink1s array
* @param {number} millis milliseconds to fade
* @param {Color} color tinycolor object
* @param {number} ledn which led, 0=all, 1-18
* @param {number} blink1idx index into blink1s array
*/
_fadeToRGB: function( millis, color, ledn, blink1idx ) {
// if the device exists
if( blink1s[blink1idx] && blink1s[blink1idx].device ) {
var crgb = color.toRgb();
try {
blink1s[blink1idx].device.fadeToRGB( millis, crgb.r, crgb.g, crgb.b, ledn );
} catch(err) {
log.error('Blink1Service._fadeToRGB: error', err);
this._removeDevice( blink1s[blink1idx].serial );
currentBlink1Id = 0; // FIXME: is this the correct response to this error?
}
}
},
// begin public functions
/**
* Return array of all blink1 serialnumbers
* @return {Array} array of blink1 serialnumbers
*/
getAllSerials: function() {
return blink1s.map(function(b1) { return b1.serial; });
},
// FIXME: support multiple blink1s
/**
* Returns true if connected, really returns number of devices connected
* @return {Number} return number of devices connected
*/
isConnected: function() {
// return (blink1serials.length > 0);
var cnt = 0;
blink1s.map( function(b1) {
if( b1.device ) { cnt++; }
});
return cnt;
},
getStatusString: function() {
//return Blink1Service.isConnected() ? "connected" : "not connected",
var cnt = this.isConnected();
return (cnt>1) ? cnt + " devices connected" : (cnt) ? "device connected" : "no device connected";
},
// FIXME: support multiple blink1s
serialNumber: function() {
if( this.isConnected() ) {
return blink1s[0].serial;
}
return '';
},
// FIXME: support multiple blink1s
serialNumberForDisplay: function() {
return (this.isConnected()) ? this.serialNumber() : '-';
},
// FIXME: fix and call this blink1Id or something
iftttKey: function() { // FIXME:
// console.log("IFTTT KEY:",this.serialNumber());
var s = this.serialNumber() || '00000000';
var k = this.hostId() + s;
return k;
},
hostId: function() {
var id = config.readSettings('hostId');
if( !id ) {
id = util.generateRandomHostId();
this.setHostId(id);
}
return id;
},
setHostId: function(id) {
config.saveSettings( 'hostId', id);
// this.notifyChange();
},
setCurrentBlink1Id(id) {
log.msg("setCurrentBlink1Id: ",id);
currentBlink1Id = id;
this.notifyChange();
},
getCurrentBlink1Id() {
return currentBlink1Id;
},
setCurrentLedN: function(n,blink1id) {
var blink1idx = this.idToBlink1Index(blink1id);
currentState[blink1idx].ledn = n;
this.notifyChange();
},
getCurrentLedN: function(blink1id) {
var blink1idx = this.idToBlink1Index(blink1id);
// blink1idx = blink1idx || 0;
return currentState[blink1idx].ledn;
},
setCurrentMillis: function(m,blink1id) {
var blink1idx = this.idToBlink1Index(blink1id);
currentState[blink1idx].millis = m;
this.notifyChange();
},
getCurrentMillis: function(blink1id) {
var blink1idx = this.idToBlink1Index(blink1id);
return currentState[blink1idx].millis;
},
getCurrentColor: function(blink1id) { // FIXME
var blink1idx = this.idToBlink1Index(blink1id);
// blink1idx = blink1idx || this.idToBlink1Index(currentBlink1Id);
var curledn = currentState[blink1idx].ledn;
curledn = (curledn>0) ? curledn-1 : curledn; // 0 = all LEDs in a blink1, so shift or use 0th element as rep
return currentState[blink1idx].colors[ curledn ];
},
// FIXME: this is confusing
getCurrentColors: function(blink1id) {
var blink1idx = this.idToBlink1Index(blink1id);
// blink1idx = blink1idx || 0;
return currentState[blink1idx].colors;
},
/**
* Lookup blink1id (blink1 serial number) and map to an index in blink1s array
* If 'blink1ToUse' is set, and blink1id is undefined or 0, use blink1ToUse
* FIXME: Should use conf.blink1Service.blink1ToUse
* FIXME: should convert serial to index
* @param {number|String} blink1id array id (0-maxblink1s) or 8-digit blink1serial
* @return {number} known-good index into blink1s array or 0
*/
idToBlink1Index: function(blink1id) {
if( blink1id === undefined || blink1id === '' ) {
if( this.conf.blink1ToUse ) {
blink1id = this.conf.blink1ToUse;
} else {
blink1id = currentBlink1Id;
}
}
// it's an array index (NOTE: SHOULD NEVER HIT THIS, BUT JUST IN CASE)
if( blink1id >= 0 && blink1id < blink1s.length ) {
return blink1id;
}
// otherwise it's a blink1 serialnumber, so search for it
var blink1idx = 0; // default to first blink1
blink1id = blink1id.toString().toUpperCase();
blink1s.map( function(b,idx) {
if( blink1id === b.serial ) {
blink1idx = idx;
}
});
return blink1idx;
},
// debug only
dumpCurrentState: function() {
var str = '';
currentState.map( function(b1state, i) {
var colorsstr = b1state.colors.reduce( function(prev,curr) { return prev +','+ curr.toHexString(); });
str += 'blink1Idx:'+i+ ': ledn:'+b1state.ledn+ ', millis:'+b1state.millis+ ', colors:'+colorsstr + ';\n';
});
return str;
},
/**
* Main entry point for this service, sets currentColor & currentLedN
* 'color' arg is a tinycolor() color or hextring ('#ff00ff')
* if color is a hexstring, it will get converted to tinycolor
* ledn is 1-index into color array, and ledn=0 means "all leds"
* blink1_id is index into blink1s array (but should also be by serialnumber)
* FIXME: currentState[0] will be currentState[blink1idx]
*
* @param {[type]} millis [description]
* @param {[type]} color [description]
* @param {[type]} ledn [description]
* @param {[type]} blink1_id [description]
* @return {[type]} [description]
*/
fadeToColor: function( millis, color, ledn, blink1_id) {
ledn = ledn || 0; // 0 == all LEDs
if( typeof color === 'string' ) {
color = tinycolor( color ); // FIXME: must be better way
}
// convert blink1_id to array index
var blink1Idx = this.idToBlink1Index(blink1_id);
// FIXME: how to make sure 'color' is a tinycolor object? color.isValid?
// log.msg('Blink1Service.fadeToColor: blink1Idx:',blink1Idx,' msec:',millis,' ledn:',ledn,
// ' c:',color.toHexString(), "currentState:\n"+ this.dumpCurrentState()); // JSON.stringify(currentState,null,2));
// var colors = _.clone(currentState[blink1Idx].colors);
var colors = currentState[blink1Idx].colors;
// handle special meaning: ledn=0 -> all LEDs
if( ledn === 0 ) { colors = colors.fill( color ); }
else { colors[ledn-1] = color; }
// FIXME: do we need these states and the blink1s struct?
// lastState[blink1idx] = currentState[blink1idx];
currentState[blink1Idx] = { ledn: ledn, millis: millis, colors: colors };
this.notifyChange();
// divide currentMillis by two to make it appear more responsive
// by having blink1 be at destination color for half the time
millis = millis/2;
// color, ledn, & blink1idx is known-good at this point
this._fadeToRGB( millis, color, ledn, blink1Idx);
},
/**
* turn off everything, or for just a specific blink1
* @method function
* @param {String} blink1id blink1 serial number or id or nothing
* @return {[type]} [description]
*/
off: function(blink1id) {
var self = this;
self.toyStop();
if( blink1id === undefined && blink1s.length > 0 ) {
blink1s.map( function(serial,idx) {
// console.log("POOP");
self.fadeToColor(100, '#000000', 0, idx);
});
} else {
self.fadeToColor(100, '#000000', 0, blink1id); // 0 = all leds
}
},
/**
* Stop the toy engine
* @param {String} mode Toy mode: 'moodlight', 'colorcycle', 'strobe'
*/
toyStop: function() {
this.toy.mode = '';
this.toy.enable = false;
if( this.toy.timer ) { clearTimeout( this.toy.timer ); }
},
/**
* Start up the toy engine
* @param {String} mode Toy mode: 'moodlight', 'colorcycle', 'strobe'
*/
toyStart: function(mode) {
if( mode === 'moodlight' ) {
this.toy.interval = 5000;
}
else if( mode === 'colorcycle' ) {
this.toy.interval = 30;
this.toy.value = Math.floor( Math.random() * 360 );
}
else if( mode === 'party' ) {
this.toy.interval = 100;
// this.toy.value = '#FFFFFF';
}
else if( mode === 'strobe' ) {
this.toy.interval = 100;
this.toy.value = '#FFFFFF';
}
else {
log.msg("Blink1Service: unknown toy mode: ",mode);
return;
}
this.toy.mode = mode;
this.toy.enable = true;
this.toyDo();
},
/**
* Actually do the toy in question
*/
toyDo: function() {
var rledn = Math.floor( Math.random() * 2 ) + 1;
var rhue = Math.floor( Math.random() * 360 );
var rcolor;
if( !this.toy.enable ) { return; }
if( this.toy.mode === 'moodlight' ) {
rcolor = tinycolor( {h:rhue, s:1, v:0.75} );
this.fadeToColor( this.toy.interval*2, rcolor, rledn );
}
else if( this.toy.mode === 'colorcycle' ) {
this.toy.value += 2;
this.toy.value = (this.toy.value>360) ? 0 : this.toy.value;
var color = tinycolor({h:this.toy.value, s:1, v:1});
this.fadeToColor( 100, color, 0 );
}
else if( this.toy.mode === 'party' ) {
rcolor = tinycolor({h:rhue, s:1, v:1});
this.fadeToColor( this.toy.interval, rcolor, rledn);
}
else if( this.toy.mode === 'strobe' ) {
this.toy.value = (this.toy.value==='#000000') ? '#FFFFFF' : '#000000' ;
this.fadeToColor( this.toy.interval, this.toy.value, 0 );
}
this.toy.timer = setTimeout(this.toyDo.bind(this), this.toy.interval);
},
addChangeListener: function(callback, callername) {
listeners[callername] = callback;
// console.log("Blink1Service: addChangelistener", listeners );
},
removeChangeListener: function(callername) {
log.msg("Blink1Service.removeChangelistener: removing", callername);
delete listeners[callername];
// log.msg("Blink1Service.removeChangelistener", listeners );
},
removeAllListeners: function() {
_.keys( listeners, function(callername) {
this.removeChangelistener(callername);
});
},
notifyChange: function() {
// log.msg("blink1service: listeners:",listeners);
_.forIn( listeners, function(callback) {
// currentColor and currentColors are tinycolor objects
// callback( Blink1Service.getCurrentColor(), currentColors, currentLedN, currentMillis);
// callback( Blink1Service.getCurrentColor(), currentState[0].colors, currentState[0].ledn, currentState[0].millis );
callback();
});
},
};
module.exports = Blink1Service;
// _closeCurrentDevice: function() {
// log.msg('Blink1Service._closeCurrentDevice: closing blink1');
// if( blink1 ) {
// blink1.close();
// blink1 = null;
// }
// },