forked from node-red/node-red-nodes
-
Notifications
You must be signed in to change notification settings - Fork 0
/
145-BBB-hardware.js
565 lines (528 loc) · 25.8 KB
/
145-BBB-hardware.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
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
module.exports = function (RED) {
"use strict";
var bonescript, adjustName, setPinMode;
var analogInputPins = ["P9_39", "P9_40", "P9_37", "P9_38", "P9_33", "P9_36", "P9_35"];
var gpioPins = ["P8_7", "P8_8", "P8_9", "P8_10", "P8_11", "P8_12", "P8_13", "P8_14", "P8_15",
"P8_16", "P8_17", "P8_18", "P8_19", "P8_26", "P9_11", "P9_12", "P9_13", "P9_14",
"P9_15", "P9_16", "P9_17", "P9_18", "P9_21", "P9_22", "P9_23", "P9_24", "P9_26",
"P9_27", "P9_30", "P9_41", "P9_42"];
var usrLEDs = ["USR0", "USR1", "USR2", "USR3"];
// Load the hardware library and set up polymorphic functions to suit it. Prefer
// octalbonescript (faster & less buggy) but drop back to bonescript if not available
bonescript = require("octalbonescript");
adjustName = function (pin) {
if (pin === "P8_7") {
pin = "P8_07";
}
else if (pin === "P8_8") {
pin = "P8_08";
}
else if (pin === "P8_9") {
pin = "P8_09";
}
return pin;
};
setPinMode = function (pin, mode, callback) {
bonescript.pinMode(pin, mode, callback);
}
// Node constructor for bbb-analogue-in
function AnalogueInputNode(n) {
RED.nodes.createNode(this, n);
var node = this;
// Store local copies of the node configuration (as defined in the .html)
this.topic = n.topic;
this.pin = n.pin; // The Beaglebone Black pin identifying string
this._pin = adjustName(this.pin); // Adjusted for Octal if necessary
this.breakpoints = n.breakpoints;
this.averaging = n.averaging;
if (this.averaging) {
this.averages = 10;
}
else {
this.averages = 1;
}
// Variables used for input averaging
var sum; // accumulates the input readings to be averaged
var count; // keep track of the number of measurements made
// The callback function for analogRead. Accumulates the required number of
// measurements, then divides the total number, applies output scaling and
// sends the result
var analogReadCallback = function (err, x) {
sum = sum + Number(x);
count = count - 1;
if (count > 0) {
bonescript.analogRead(node._pin, analogReadCallback);
}
else {
var msg = {};
msg.topic = node.topic;
sum = sum/node.averages;
// i is the index of the first breakpoint where the 'input' value is strictly
// greater than the measurement (note: a measurement can never be == 1)
var i = node.breakpoints.map(function (breakpoint) {
return sum >= breakpoint.input;
}).indexOf(false);
msg.payload = node.breakpoints[i - 1].output + (node.breakpoints[i].output - node.breakpoints[i - 1].output)*
(sum - node.breakpoints[i - 1].input)/(node.breakpoints[i].input - node.breakpoints[i - 1].input);
node.send(msg);
}
};
// If we have a valid pin, set the input event handler to Bonescript's analogRead
if (analogInputPins.indexOf(node.pin) >= 0) {
node.on("input", function () {
sum = 0;
count = node.averages;
bonescript.analogRead(node._pin, analogReadCallback);
});
}
else {
node.error("Unconfigured input pin");
}
}
// Node constructor for bbb-discrete-in
function DiscreteInputNode(n) {
RED.nodes.createNode(this, n);
var node = this;
// Store local copies of the node configuration (as defined in the .html)
this.topic = n.topic; // the topic is not currently used
this.pin = n.pin; // The Beaglebone Black pin identifying string
this._pin = adjustName(this.pin); // Adjusted for Octal if necessary
if (n.activeLow) { // Set the 'active' state 0 or 1 as appropriate
this.activeState = 0;
}
else {
this.activeState = 1;
}
this.updateInterval = n.updateInterval*1000; // How often to send totalActiveTime messages
this.debounce = n.debounce || null; // Enable switch contact debouncing algorithm
if (n.outputOn === "rising") {
this.activeEdges = [false, true];
}
else if (n.outputOn === "falling") {
this.activeEdges = [true, false];
}
else if (n.outputOn === "both") {
this.activeEdges = [true, true];
}
else {
node.error("Invalid edge type: " + n.outputOn);
}
// Working variables
this.interruptAttached = false; // Flag: should we detach interrupt when we are closed?
this.intervalId = null; // Remember the timer ID so we can delete it when we are closed
this.currentState = 0; // The pin input state "1" or "0"
this.lastActiveTime = NaN; // The date (in ms since epoch) when the pin last went high
// switch to process.hrtime()
this.totalActiveTime = 0; // The total time in ms that the pin has been high (since reset)
this.starting = true;
this.debouncing = false; // True after a change of state while waiting for the 7ms debounce time to elapse
this.debounceTimer = null;
// This function is called by the input pin change-of-state interrupt. If
// debounce is disabled, send the output message. Otherwise, if we are
// currently debouncing, ignore this interrupt. If we are not debouncing,
// schedule a re-read of the input pin in 7ms time, and set the debouncing flag
// Note: if x has an 'attached' field and no 'value' field, the callback is reporting
// the success or failure of attaching the interrupt - we must handle this
var interruptCallback = function (err, x) {
if (x === undefined) {
if (x.attached === true) {
node.interruptAttached = true;
node.on("input", inputCallback);
node.intervalId = setInterval(timerCallback, node.updateInterval);
}
else {
node.error("Failed to attach interrupt");
}
}
else if (node.currentState !== Number(x)) {
if (node.debounce) {
if (node.debouncing === false) {
node.debouncing = true;
node.debounceTimer = setTimeout(function () {
bonescript.digitalRead(node._pin, debounceCallback);
}, Number(node.debounce));
}
}
else {
sendStateMessage(x);
}
}
};
// This function is called approx 7ms after a potential change-of-state which is
// being debounced. Terminate the debounce, and send a message if the state has
// actually changed
var debounceCallback = function (err, x) {
node.debounceTimer = null;
node.debouncing = false;
if (x !== undefined && node.currentState !== Number(x)) {
sendStateMessage(x);
}
};
// This function is called when either the interruptCallback or the debounceCallback
// have determined we have a 'genuine' change of state. Update the currentState and
// ActiveTime variables, and send a message on the first output with the new state
var sendStateMessage = function (x) {
node.currentState = Number(x);
var now = Date.now();
if (node.currentState === node.activeState) {
node.lastActiveTime = now;
}
else if (!isNaN(node.lastActiveTime)) {
node.totalActiveTime += now - node.lastActiveTime;
}
if (node.activeEdges[node.currentState]) {
var msg = {};
msg.topic = node.topic;
msg.payload = node.currentState;
node.send([msg, null]);
}
};
// This function is called by the timer. It updates the ActiveTime variables, and sends a
// message on the second output with the latest value of the total active time, in seconds
var timerCallback = function () {
if (node.currentState === node.activeState) {
var now = Date.now();
node.totalActiveTime += now - node.lastActiveTime;
node.lastActiveTime = now;
}
var msg = {};
msg.topic = node.topic;
msg.payload = node.totalActiveTime/1000;
node.send([null, msg]);
// Re-synchronise the pin state if we have missed a state change interrupt for some
// reason, and we are not in the process of debouncing one
if (node.debouncing === false) {
bonescript.digitalRead(node._pin, interruptCallback);
}
};
// This function is called when we receive an input message. If the topic contains
// 'load' (case insensitive) set the totalActiveTime to the numeric value of the
// payload, if possible. Otherwise clear the totalActiveTime (so we start counting
// from zero again)
var inputCallback = function (ipMsg) {
if (String(ipMsg.topic).search(/load/i) < 0 || isFinite(ipMsg.payload) === false) {
node.totalActiveTime = 0;
}
else {
node.totalActiveTime = Number(ipMsg.payload);
}
if (node.currentState === node.activeState) {
node.lastActiveTime = Date.now();
}
// On startup, send an initial activeTime message, but only send an
// initial currentState message if we are in both edges active mode
if (node.starting) {
node.starting = false;
var msg;
if (node.activeEdges[0] && node.activeEdges[1]) {
msg = [{topic: node.topic}, {topic: node.topic}];
msg[0].payload = node.currentState;
}
else {
msg = [null, {topic: node.topic}];
}
msg[1].payload = node.totalActiveTime;
node.send(msg);
}
};
// If we have a valid pin, set it as an input and read the (digital) state
if (gpioPins.indexOf(node.pin) >= 0) {
// Don't set up interrupts & intervals until after the close event handler has been installed
bonescript.detachInterrupt(node._pin);
process.nextTick(function () {
setPinMode(node._pin, bonescript.INPUT, function (response, pin) {
if (!response) {
bonescript.digitalRead(node._pin, function (err, x) {
// Initialise the currentState and lastActiveTime variables based on the value read
node.currentState = Number(x);
if (node.currentState === node.activeState) {
node.lastActiveTime = Date.now();
}
// Attempt to attach a change-of-state interrupt handler to the pin. If we succeed,
// the input event and interval handlers will be installed by interruptCallback
bonescript.attachInterrupt(node._pin, bonescript.CHANGE, interruptCallback);
// Send an initial message with the pin state on the first output
setTimeout(function () {
node.emit("input", {});
}, 50);
});
}
else {
node.error("Unable to set " + pin + " as input: " + response);
}
});
});
}
else {
node.error("Unconfigured input pin");
}
}
// Node constructor for bbb-pulse-in
function PulseInputNode(n) {
RED.nodes.createNode(this, n);
var node = this;
// Store local copies of the node configuration (as defined in the .html)
this.topic = n.topic; // the topic is not currently used
this.pin = n.pin; // The Beaglebone Black pin identifying string
this._pin = adjustName(this.pin); // Adjusted for Octal if necessary
this.updateInterval = n.updateInterval*1000; // How often to send output messages
this.countType = n.countType; // Sets either 'edge' or 'pulse' counting
this.countUnit = n.countUnit; // Scaling applied to count output
this.countRate = n.countRate; // Scaling applied to rate output
// Working variables
this.interruptAttached = false; // Flag: should we detach interrupt when we are closed?
this.intervalId = null; // Remember the timer ID so we can delete it when we are closed
this.pulseCount = 0; // (Unscaled) total pulse count
// Hold the hrtime of the last two pulses (with ns resolution)
this.pulseTime = [[NaN, NaN], [NaN, NaN]];
// Called by the edge or pulse interrupt. Record the pulse time and count the pulse
// Note: if x has an 'attached' field and no 'value' field, the callback is reporting
// the success or failure of attaching the interrupt - we must handle this
var interruptCallback = function (x) {
if (x === undefined) {
if (x.attached === true) {
node.interruptAttached = true;
node.on("input", inputCallback);
node.intervalId = setInterval(timerCallback, node.updateInterval);
}
else {
node.error("Failed to attach interrupt");
}
}
else {
node.pulseTime = [node.pulseTime[1], process.hrtime()];
node.pulseCount = node.pulseCount + 1;
}
};
// Called when an input message arrives. If the topic contains 'load' (case
// insensitive) and the payload is a valid number, set the count to that
// number, otherwise set it to zero
var inputCallback = function (msg) {
if (String(msg.topic).search(/load/i) < 0 || isFinite(msg.payload) === false) {
node.pulseCount = 0;
}
else {
node.pulseCount = Number(msg.payload);
}
};
// Called by the message timer. Send two messages: the scaled pulse count on
// the first output and the scaled instantaneous pulse rate on the second.
// The instantaneous pulse rate is the reciprocal of the larger of either the
// time interval between the last two pulses, or the time interval since the last pulse.
var timerCallback = function () {
var now = process.hrtime();
var lastTime = node.pulseTime[1][0] - node.pulseTime[0][0] + (node.pulseTime[1][1] - node.pulseTime[0][1])/1e9;
var thisTime = now[0] - node.pulseTime[1][0] + (now[1] - node.pulseTime[1][1])/1e9;
var msg = [{topic: node.topic}, {topic: node.topic}];
msg[0].payload = node.countUnit*node.pulseCount;
// At startup, pulseTime contains NaN's: force the rate output to 0
msg[1].payload = node.countRate/Math.max(thisTime, lastTime) || 0;
node.send(msg);
};
// If we have a valid pin, set it as an input and read the (digital) state
if (gpioPins.indexOf(node.pin) >= 0) {
// Don't set up interrupts & intervals until after the close event handler has been installed
bonescript.detachInterrupt(node._pin);
process.nextTick(function () {
setPinMode(node._pin, bonescript.INPUT, function (response, pin) {
if (!response) {
bonescript.digitalRead(node._pin, function (err, x) {
// Initialise the currentState based on the value read
node.currentState = Number(x);
// Attempt to attach an interrupt handler to the pin. If we succeed,
// set the input event and interval handlers
var interruptType;
if (node.countType === "pulse") {
// interruptType = bonescript.FALLING; <- doesn't work in v0.2.4
interruptType = bonescript.RISING;
}
else {
interruptType = bonescript.CHANGE;
}
// Attempt to attach the required interrupt handler to the pin. If we succeed,
// the input event and interval handlers will be installed by interruptCallback
bonescript.attachInterrupt(node._pin, interruptType, interruptCallback)
});
}
else {
node.error("Unable to set " + pin + " as input: " + response);
}
});
});
}
else {
node.error("Unconfigured input pin");
}
}
// Node constructor for bbb-discrete-out
function DiscreteOutputNode(n) {
RED.nodes.createNode(this, n);
var node = this;
// Store local copies of the node configuration (as defined in the .html)
this.topic = n.topic; // the topic is not currently used
this.pin = n.pin; // The Beaglebone Black pin identifying string
this._pin = adjustName(this.pin); // Adjusted for Octal if necessary
this.defaultState = Number(n.defaultState); // What state to set up as
this.inverting = n.inverting;
this.toggle = n.toggle;
// Working variables
this.currentState = this.defaultState;
// If the input message payload is numeric, values > 0.5 are 'true', otherwise use
// the truthiness of the payload. Apply the inversion flag before setting the output
var inputCallback = function (msg) {
var newState;
if (node.toggle) {
newState = node.currentState === 0 ? 1 : 0;
}
else {
if (isFinite(Number(msg.payload))) {
newState = Number(msg.payload) > 0.5;
}
else if (msg.payload) {
newState = true;
}
else {
newState = false;
}
if (node.inverting) {
newState = !newState;
}
}
bonescript.digitalWrite(node._pin, newState ? 1 : 0, function() {
node.send({topic: node.topic, payload: newState});
node.currentState = newState;
});
};
// If we have a valid pin, set it as an output and set the default state
if (gpioPins.concat(usrLEDs).indexOf(node.pin) >= 0) {
// Don't set up interrupts & intervals until after the close event handler has been installed
if (node._pin) { bonescript.detachInterrupt(node._pin); }
process.nextTick(function () {
setPinMode(node._pin, bonescript.OUTPUT, function (response, pin) {
if (response) {
node.error("Unable to set " + pin + " as output: " + response.err);
}
else {
node.on("input", inputCallback);
setTimeout(function () {
bonescript.digitalWrite(node._pin, node.defaultState, function() {});
}, 50);
}
});
});
}
else {
node.error("Unconfigured output pin");
}
}
// Node constructor for bbb-pulse-out
function PulseOutputNode(n) {
RED.nodes.createNode(this, n);
var node = this;
// Store local copies of the node configuration (as defined in the .html)
this.topic = n.topic; // the topic is not currently used
this.pin = n.pin; // The Beaglebone Black pin identifying string
this._pin = adjustName(this.pin); // Adjusted for Octal if necessary
this.pulseState = Number(n.pulseState); // What state the pulse will be..
this.defaultState = this.pulseState === 1 ? 0 : 1;
this.retriggerable = n.retriggerable;
this.pulseTime = n.pulseTime*1000; // Pulse width in milliseconds
// Working variables
this.pulseTimer = null; // Non-null while a pulse is being generated
// Generate a pulse in response to an input message. If the topic includes the text
// 'time' (case insensitive) and the payload is numeric, use this value as the
// pulse time. Otherwise use the value from the properties dialog.
// If the resulting pulse time is < 1ms, do nothing.
// If the pulse mode is not retriggerable, then if no pulseTimer is active, generate
// a pulse. If the pulse mode is retriggerable, and a pulseTimer is active, cancel it.
// If no timer is active, set the pulse output. In both cases schedule a new pulse
// timer.
var inputCallback = function (msg) {
var time = node.pulseTime;
if (String(msg.topic).search(/time/i) >= 0 && isFinite(msg.payload)) {
time = msg.payload*1000;
}
if (time >= 1) {
if (node.retriggerable === false) {
if (node.pulseTimer === null) {
node.pulseTimer = setTimeout(endPulseCallback, time);
bonescript.digitalWrite(node._pin, node.pulseState, function() {
node.send({topic: node.topic, payload: node.pulseState});
});
}
}
else {
if (node.pulseTimer !== null) {
clearTimeout(node.pulseTimer);
}
else {
bonescript.digitalWrite(node._pin, node.pulseState, function() {
node.send({topic: node.topic, payload: node.pulseState});
});
}
node.pulseTimer = setTimeout(endPulseCallback, time);
}
}
};
// At the end of the pulse, restore the default state and set the timer to null
var endPulseCallback = function () {
node.pulseTimer = null;
bonescript.digitalWrite(node._pin, node.defaultState, function() {
node.send({topic: node.topic, payload: node.defaultState});
});
};
// If we have a valid pin, set it as an output and set the default state
if (gpioPins.concat(usrLEDs).indexOf(node.pin) >= 0) {
// Don't set up interrupts & intervals until after the close event handler has been installed
bonescript.detachInterrupt(node._pin);
process.nextTick(function () {
setPinMode(node._pin, bonescript.OUTPUT, function (response, pin) {
if (!response) {
node.on("input", inputCallback);
// Set the pin to the default state once the dust settles
setTimeout(endPulseCallback, 50);
}
else {
node.error("Unable to set " + pin + " as output: " + response.err);
}
});
});
}
else {
node.error("Unconfigured output pin");
}
}
// Register the nodes by name. This must be called before overriding any of the Node functions.
RED.nodes.registerType("bbb-analogue-in", AnalogueInputNode);
RED.nodes.registerType("bbb-discrete-in", DiscreteInputNode);
RED.nodes.registerType("bbb-pulse-in", PulseInputNode);
RED.nodes.registerType("bbb-discrete-out", DiscreteOutputNode);
RED.nodes.registerType("bbb-pulse-out", PulseOutputNode);
// On close, detach the interrupt (if we attached one) and clear any active timers
DiscreteInputNode.prototype.close = function () {
if (this.interruptAttached) {
bonescript.detachInterrupt(this._pin);
}
if (this.intervalId !== null) {
clearInterval(this.intervalId);
}
if (this.debounceTimer !== null) {
clearTimeout(this.debounceTimer);
}
};
// On close, detach the interrupt (if we attached one) and clear the interval (if we set one)
PulseInputNode.prototype.close = function () {
if (this.interruptAttached) {
bonescript.detachInterrupt(this._pin);
}
if (this.intervalId !== null) {
clearInterval(this.intervalId);
}
};
// On close, clear an active pulse timer
PulseOutputNode.prototype.close = function () {
if (this.pulseTimer !== null) {
clearTimeout(this.pulseTimer);
}
};
};