UNPKG

32.5 kBJavaScriptView Raw
1/* istanbul ignore if */
2if (!Array.prototype.includes) {
3 /* istanbul ignore next */
4 require("./array-includes-shim");
5}
6
7
8var IS_TEST_MODE = !!process.env.IS_TEST_MODE;
9var Emitter = require("events").EventEmitter;
10var util = require("util");
11var chalk = require("chalk");
12var Collection = require("./mixins/collection");
13var Fn = require("./fn");
14var Repl = require("./repl");
15var Options = require("./board.options");
16var Pins = require("./board.pins");
17var Expander;
18//var temporal = require("temporal");
19
20// Environment Setup
21var boards = [];
22var rport = /usb|acm|^com/i;
23
24// const things to const when 0.10.x is dropped
25// This string appears over 20 times in this file.
26var UNDEFINED = "undefined";
27
28var Serial = {
29 used: [],
30 attempts: [],
31 detect: function(callback) {
32 // Max number of times listing serial conntections can fail
33 var maxAttempts = 10;
34 // Delay (ms) before trying again to list serial connections
35 var retryDelay = 400;
36 var serialport;
37
38 /* istanbul ignore next */
39 if (parseFloat(process.versions.nw) >= 0.13) {
40 serialport = require("browser-serialport");
41 } else {
42 serialport = require("serialport");
43 }
44
45 // console.log(require);
46 // Request a list of available ports, from
47 // the result set, filter for valid paths
48 // via known path pattern match.
49 serialport.list(function(err, result) {
50
51 // serialport.list() will never result in an error.
52 // On failure, an empty array is returned. (#768)
53 var ports = result.filter(function(val) {
54 var available = true;
55
56 // Match only ports that Arduino cares about
57 // ttyUSB#, cu.usbmodem#, COM#
58 if (!rport.test(val.comName)) {
59 available = false;
60 }
61
62 // Don't allow already used/encountered usb device paths
63 if (Serial.used.includes(val.comName)) {
64 available = false;
65 }
66
67 return available;
68 }).map(function(val) {
69 return val.comName;
70 });
71
72 // If no ports are detected...
73 if (!ports.length) {
74
75 /* istanbul ignore if */
76 if (IS_TEST_MODE && this.abort) {
77 /* istanbul ignore next */
78 return;
79 }
80
81 // Create an attempt counter
82 /* istanbul ignore else */
83 if (!Serial.attempts[Serial.used.length]) {
84 Serial.attempts[Serial.used.length] = 0;
85
86 // Log notification...
87 this.info("Board", "Looking for connected device");
88 }
89
90 // Set the attempt number
91 Serial.attempts[Serial.used.length]++;
92
93 // Retry Serial connection
94 if (Serial.attempts[Serial.used.length] > maxAttempts) {
95 this.fail("Board", "No connected device found");
96 return;
97 }
98 setTimeout(() => {
99 Serial.detect.call(this, callback);
100 }, retryDelay);
101
102 return;
103 }
104
105 this.info("Available", chalk.grey(ports));
106
107 // Get the first available device path
108 // from the list of detected ports
109
110 callback.call(this, ports[0]);
111 }.bind(this));
112 },
113
114 connect: function(portOrPath, callback) {
115 var IO = require("firmata").Board;
116
117 var caught = null;
118 var io, isConnected, path, type;
119
120 if (typeof portOrPath === "object" && portOrPath.path) {
121 //
122 // Board({ port: SerialPort Object })
123 //
124 path = portOrPath.path;
125
126 this.info(
127 (portOrPath.transport || "SerialPort"),
128 chalk.grey(path)
129 );
130 } else {
131 //
132 // Board({ port: path String })
133 //
134 // Board()
135 // ie. auto-detected
136 //
137 path = portOrPath;
138 }
139
140 // Add the usb device path to the list of device paths that
141 // are currently in use - this is used by the filter function
142 // above to remove any device paths that we've already encountered
143 // or used to avoid blindly attempting to reconnect on them.
144 Serial.used.push(path);
145
146 try {
147 io = new IO(portOrPath, function(error) {
148 if (error) {
149 caught = error;
150 }
151
152 callback.call(this, caught, caught ? "error" : "ready", io);
153 }.bind(this));
154
155 // Extend io instance with special expandos used
156 // by Johny-Five for the IO Plugin system.
157 io.name = "Firmata";
158 io.defaultLed = 13;
159 io.port = path;
160
161 // Made this far, safely connected
162 isConnected = true;
163 } catch (error) {
164 caught = error;
165 }
166
167 if (caught) {
168 caught = caught.message || caught;
169 }
170
171 // Determine the type of event that will be passed on to
172 // the board emitter in the callback passed to Serial.detect(...)
173 type = isConnected ? "connect" : "error";
174
175 // Execute "connect" callback
176 callback.call(this, caught, type, io);
177 }
178};
179
180/**
181 * Board
182 * @constructor
183 *
184 * @param {Object} opts
185 */
186
187function Board(opts) {
188
189 if (!(this instanceof Board)) {
190 return new Board(opts);
191 }
192
193 // Ensure opts is an object
194 opts = opts || {};
195
196 // Used to define the board instance's own
197 // properties in the REPL's scope.
198 var replContext = {};
199
200 // It's feasible that an IO-Plugin may emit
201 // "connect" and "ready" events out of order.
202 // This is used to enforce the order, by
203 // postponing the "ready" event if the IO-Plugin
204 // hasn't emitted a "connect" event. Once
205 // the "connect" event is emitted, the
206 // postponement is lifted and the board may
207 // proceed with emitting the events in the
208 // correct order.
209 var isPostponed = false;
210
211 // Initialize this Board instance with
212 // param specified properties.
213 Object.assign(this, opts);
214
215 this.timer = null;
216
217 this.isConnected = false;
218
219 // Easily track state of hardware
220 this.isReady = false;
221
222 // Initialize instance property to reference io board
223 this.io = this.io || null;
224
225 // Registry of components
226 this.register = [];
227
228 // Pins, Addr (alt Pin name), Addresses
229 this.occupied = [];
230
231 // Registry of drivers by address (i.e. I2C Controllers)
232 this.Drivers = {};
233
234 // Identify for connect hardware cache
235 if (!this.id) {
236 this.id = Fn.uid();
237 }
238
239 // If no debug flag, default to true
240 if (typeof this.debug === UNDEFINED) {
241 this.debug = true;
242 }
243
244 // If no repl flag, default to true
245 if (typeof this.repl === UNDEFINED) {
246 this.repl = true;
247 }
248
249 // If no sigint flag, default to true
250 if (typeof this.sigint === UNDEFINED) {
251 this.sigint = true;
252 }
253
254 // Specially processed pin capabilities object
255 // assigned when physical board has reported
256 // "ready" via Firmata or IO-Plugin.
257 this.pins = null;
258
259 // Create a Repl instance and store as
260 // instance property of this io/board.
261 // This will reduce the amount of boilerplate
262 // code required to _always_ have a Repl
263 // session available.
264 //
265 // If a sesssion exists, use it
266 // (instead of creating a new session)
267 //
268 /* istanbul ignore if */
269 if (this.repl) {
270 /* istanbul ignore if */
271 if (Repl.ref) {
272 /* istanbul ignore next */
273 replContext[this.id] = this;
274 /* istanbul ignore next */
275 Repl.ref.on("ready", function() {
276 /* istanbul ignore next */
277 Repl.ref.inject(replContext);
278 });
279 /* istanbul ignore next */
280 this.repl = Repl.ref;
281 } else {
282 replContext[this.id] = replContext.board = this;
283 this.repl = new Repl(replContext);
284 }
285 }
286
287 if (opts.io) {
288 // If you already have a connected io instance
289 this.io = opts.io;
290 this.isReady = opts.io.isReady;
291 this.transport = this.io.transport || null;
292 this.port = this.io.name;
293 this.pins = Board.Pins(this);
294 this.RESOLUTION = this.io.RESOLUTION || { ADC: 1023, DAC: null, PWM: 255 };
295 } else {
296
297 if (this.port && opts.port) {
298 Serial.connect.call(this, this.port, finalizeAndBroadcast);
299 } else {
300 Serial.detect.call(this, function(path) {
301 Serial.connect.call(this, path, finalizeAndBroadcast);
302 });
303 }
304 }
305
306 // Either an IO instance was provided or isOnBoard is true
307 if (!opts.port && this.io !== null) {
308 /* istanbul ignore next */
309 this.info("Available", chalk.grey(this.io.name || "unknown"));
310
311 ["connect", "ready"].forEach(function(type) {
312 this.io.once(type, function() {
313 // Since connection and readiness happen asynchronously,
314 // it's actually possible for Johnny-Five to receive the
315 // events out of order and that should be ok.
316 if (type === "ready" && !this.isConnected) {
317 isPostponed = true;
318 } else {
319 // Will emit the "connect" and "ready" events
320 // if received in order. If out of order, this
321 // will only emit the "connect" event. The
322 // "ready" event will be handled in the next
323 // condition's consequent.
324 finalizeAndBroadcast.call(this, null, type, this.io);
325 }
326
327 if (type === "connect" && isPostponed) {
328 finalizeAndBroadcast.call(this, null, "ready", this.io);
329 }
330 }.bind(this));
331
332 if (this.io.isReady) {
333 // If the IO instance is reached "ready"
334 // state, queue tick tasks to emit the
335 // "connect" and "ready" events
336 process.nextTick(function() {
337 this.io.emit(type);
338 }.bind(this));
339 }
340 }, this);
341 }
342
343 this.once("ready", function() {
344 var hrstart = process.hrtime();
345
346 this.millis = function() {
347 var now = process.hrtime(hrstart);
348 return (now[1] / 1000000);
349 };
350
351 ["close", "disconnect", "error", "string"].forEach(function(type) {
352 this.io.on(type, function(data) {
353 this.emit(type, data);
354 }.bind(this));
355 }, this);
356 }.bind(this));
357
358 // Cache instance to allow access from module constructors
359 boards.push(this);
360}
361
362function finalizeAndBroadcast(data, type, io) {
363 var emitted = false;
364
365 // Assign found io to instance
366 if (!this.io) {
367 this.io = io;
368 }
369
370 // Always Surface errors
371 if (type === "error") {
372 /* istanbul ignore else */
373 if (data && data.message) {
374 emitted = true;
375 this.error("Error", data.message);
376 }
377 }
378
379 if (type === "connect") {
380 this.isConnected = true;
381 this.port = io.port || io.name;
382
383 this.info(
384 "Connected",
385 chalk.grey(this.port)
386 );
387
388 // Unless a "timeout" value has been provided apply 10 Second timeout...
389 //
390 // If "ready" hasn't fired and cleared the timer within
391 // 10 seconds of the connect event, then it's likely
392 // there is an issue with the device or firmware.
393 if (!IS_TEST_MODE) {
394 /* istanbul ignore next */
395 this.timer = setTimeout(function() {
396 this.error(
397 "Device or Firmware Error",
398
399 "A timeout occurred while connecting to the Board. \n\n" +
400 "Please check that you've properly flashed the board with the correct firmware.\n" +
401 "See: https://github.com/rwaldron/johnny-five/wiki/Getting-Started#trouble-shooting\n\n" +
402 "If connecting to a Leonardo or Leonardo clone, press the 'Reset' button on the " +
403 "board, wait approximately 11 seconds for complete reset, then run your program again."
404 );
405
406 this.emit("error", new Error("A timeout occurred while connecting to the Board."));
407 }.bind(this), this.timeout || 1e4);
408 }
409 }
410
411 if (type === "ready") {
412 if (this.timer) {
413 clearTimeout(this.timer);
414 }
415
416 // Update instance `ready` flag
417 this.isReady = true;
418 this.pins = Board.Pins(this);
419 this.MODES = this.io.MODES;
420
421 if (typeof io.debug !== UNDEFINED &&
422 io.debug === false) {
423 this.debug = false;
424 }
425
426 if (typeof io.repl !== UNDEFINED &&
427 io.repl === false) {
428 this.repl = false;
429 }
430 // In multi-board mode, block the REPL from
431 // activation. This will be started directly
432 // by the Board.Collection constructor.
433 //
434 // In single-board mode, the REPL will not
435 // be blocked at all.
436 //
437 // If the user program has not disabled the
438 // REPL, initialize it.
439 if (this.repl) {
440 this.repl.initialize(this.emit.bind(this, "ready"));
441 }
442
443 if (io.name !== "Mock" && this.sigint) {
444 process.on("SIGINT", function() {
445 // Time to wait before forcing exit
446 var failExitTimeout = 1000;
447
448 this.emit("exit");
449 this.warn("Board", "Closing.");
450 /* istanbul ignore next */
451 var timeout = setTimeout(function() {
452 process.reallyExit();
453 }, failExitTimeout);
454 var interval = setInterval(function() {
455 if (!this.io.pending) {
456 clearInterval(interval);
457 clearTimeout(timeout);
458 process.nextTick(process.reallyExit);
459 }
460 }.bind(this), 1);
461 }.bind(this));
462 }
463
464 // Older versions of Firmata and some IO plugins
465 // may not have set RESOLUTION.
466 this.RESOLUTION = io.RESOLUTION || { ADC: 1023, DAC: null, PWM: 255 };
467
468 }
469
470 // If there is a REPL...
471 if (this.repl) {
472 // "ready" will be emitted once repl.initialize
473 // is complete, so the only event that needs to
474 // be propagated here is the "connect" event.
475 if (type === "connect") {
476 this.emit(type, data);
477 }
478 } else {
479 // The REPL is disabled, propagate all events
480 if (!emitted) {
481 this.emit(type, data);
482 }
483 }
484}
485
486// Inherit event api
487util.inherits(Board, Emitter);
488
489
490
491/**
492 * Pass through methods
493 */
494[
495 "digitalWrite", "analogWrite",
496 "analogRead", "digitalRead",
497 "pinMode", "queryPinState",
498 "stepperConfig", "stepperStep",
499 "sendI2CConfig", "sendI2CWriteRequest", "sendI2CReadRequest",
500 "i2cConfig", "i2cWrite", "i2cWriteReg", "i2cRead", "i2cReadOnce",
501 "pwmWrite",
502 "servoConfig", "servoWrite",
503 "sysexCommand", "sysexResponse",
504 "serialConfig", "serialWrite", "serialRead", "serialStop", "serialClose", "serialFlush", "serialListen",
505].forEach(function(method) {
506 /* istanbul ignore next */
507 Board.prototype[method] = function() {
508 this.io[method].apply(this.io, arguments);
509 return this;
510 };
511});
512
513
514Board.prototype.snapshot = function(reducer) {
515 var blacklist = this.snapshot.blacklist;
516 var special = this.snapshot.special;
517 var hasReducer = typeof reducer === "function";
518
519 return this.register.reduce(function(accum, component) {
520 // Don't include collections or multi/imu wrappers
521 if (typeof component.components === UNDEFINED) {
522 accum.push(
523 Object.getOwnPropertyNames(component).reduce(function(data, prop) {
524 var value = component[prop];
525
526 if (!blacklist.includes(prop) && typeof value !== "function") {
527
528 if (hasReducer) {
529 var result = reducer(prop, value, component);
530
531 if (result !== undefined) {
532 data[prop] = result;
533 }
534 } else {
535 data[prop] = special[prop] ?
536 special[prop](value) : value;
537 }
538 }
539 return data;
540 }, Object.create(null))
541 );
542 }
543
544 return accum;
545 }.bind(this), []);
546};
547
548Board.prototype.serialize = function(reducer) {
549 return JSON.stringify(this.snapshot(reducer));
550};
551
552Board.prototype.snapshot.blacklist = [
553 "board", "io", "_events", "_eventsCount", "state",
554];
555
556Board.prototype.samplingInterval = function(ms) {
557
558 if (this.io.setSamplingInterval) {
559 this.io.setSamplingInterval(ms);
560 } else {
561 throw new Error("This IO plugin does not implement an interval adjustment method");
562 }
563 return this;
564};
565
566
567Board.prototype.snapshot.special = {
568 mode: function(value) {
569 return ["INPUT", "OUTPUT", "ANALOG", "PWM", "SERVO"][value] || "unknown";
570 }
571};
572
573/**
574 * shiftOut
575 *
576 */
577Board.prototype.shiftOut = function(dataPin, clockPin, isBigEndian, value) {
578 if (arguments.length === 3) {
579 value = isBigEndian;
580 isBigEndian = true;
581 }
582
583 for (var i = 0; i < 8; i++) {
584 this.io.digitalWrite(clockPin, 0);
585 if (isBigEndian) {
586 this.io.digitalWrite(dataPin, !!(value & (1 << (7 - i))) | 0);
587 } else {
588 this.io.digitalWrite(dataPin, !!(value & (1 << i)) | 0);
589 }
590 this.io.digitalWrite(clockPin, 1);
591 }
592};
593
594var logging = {
595 specials: [
596 "error",
597 "fail",
598 "warn",
599 "info",
600 ],
601 colors: {
602 log: "white",
603 error: "red",
604 fail: "inverse",
605 warn: "yellow",
606 info: "cyan"
607 }
608};
609
610Board.prototype.log = function( /* type, klass, message [, long description] */ ) {
611 var args = Array.from(arguments);
612
613 // If this was a direct call to `log(...)`, make sure
614 // there is a correct "type" to emit below.
615 if (!logging.specials.includes(args[0])) {
616 args.unshift("log");
617 }
618
619 var type = args.shift();
620 var klass = args.shift();
621 var message = args.shift();
622 var color = logging.colors[type];
623 var now = Date.now();
624 var event = {
625 type: type,
626 timestamp: now,
627 class: klass,
628 message: "",
629 data: null,
630 };
631
632 if (typeof args[args.length - 1] === "object") {
633 event.data = args.pop();
634 }
635
636 message += " " + args.join(", ");
637 event.message = message.trim();
638
639 /* istanbul ignore if */
640 if (this.debug) {
641 /* istanbul ignore next */
642 console.log([
643 // Timestamp
644 chalk.grey(now),
645 // Module, color matches type of log
646 chalk.magenta(klass),
647 // Details
648 chalk[color](message),
649 // Miscellaneous args
650 args.join(", ")
651 ].join(" "));
652 }
653
654 this.emit(type, event);
655 this.emit("message", event);
656};
657
658
659// Make shortcuts to all logging methods
660logging.specials.forEach(function(type) {
661 Board.prototype[type] = function() {
662 var args = [].slice.call(arguments);
663 args.unshift(type);
664
665 this.log.apply(this, args);
666 };
667});
668
669
670/**
671 * delay, loop, queue
672 *
673 * Pass through methods to temporal
674 */
675/*
676[
677 "delay", "loop", "queue"
678].forEach(function( method ) {
679 Board.prototype[ method ] = function( time, callback ) {
680 temporal[ method ]( time, callback );
681 return this;
682 };
683});
684
685// Alias wait to delay to match existing Johnny-five API
686Board.prototype.wait = Board.prototype.delay;
687*/
688
689// -----THIS IS A TEMPORARY FIX UNTIL THE ISSUES WITH TEMPORAL ARE RESOLVED-----
690// Aliasing.
691// (temporary, while ironing out API details)
692// The idea is to match existing hardware programming apis
693// or simply find the words that are most intuitive.
694
695// Eventually, there should be a queuing process
696// for all new callbacks added
697//
698// TODO: Repalce with temporal or compulsive API
699
700Board.prototype.wait = function(time, callback) {
701 setTimeout(callback, time);
702 return this;
703};
704
705Board.prototype.loop = function(time, callback) {
706 var handler = function() {
707 callback(function() {
708 clearInterval(interval);
709 });
710 };
711 var interval = setInterval(handler, time);
712 return this;
713};
714
715// ----------
716// Static API
717// ----------
718
719// Board.map( val, fromLow, fromHigh, toLow, toHigh )
720//
721// Re-maps a number from one range to another.
722// Based on arduino map()
723Board.map = Fn.map;
724Board.fmap = Fn.fmap;
725
726// Board.constrain( val, lower, upper )
727//
728// Constrains a number to be within a range.
729// Based on arduino constrain()
730Board.constrain = Fn.constrain;
731
732// Board.range( upper )
733// Board.range( lower, upper )
734// Board.range( lower, upper, tick )
735//
736// Returns a new array range
737//
738Board.range = Fn.range;
739
740// Board.range.prefixed( prefix, upper )
741// Board.range.prefixed( prefix, lower, upper )
742// Board.range.prefixed( prefix, lower, upper, tick )
743//
744// Returns a new array range, each value prefixed
745//
746Board.range.prefixed = Fn.range.prefixed;
747
748// Board.uid()
749//
750// Returns a reasonably unique id string
751//
752Board.uid = Fn.uid;
753
754// Board.mount()
755// Board.mount( index )
756// Board.mount( object )
757//
758// Return hardware instance, based on type of param:
759// @param {arg}
760// object, user specified
761// number/index, specified in cache
762// none, defaults to first in cache
763//
764// Notes:
765// Used to reduce the amount of boilerplate
766// code required in any given module or program, by
767// giving the developer the option of omitting an
768// explicit Board reference in a module
769// constructor's options
770Board.mount = function(arg) {
771 var index = typeof arg === "number" && arg,
772 hardware;
773
774 // board was explicitly provided
775 if (arg && arg.board) {
776 return arg.board;
777 }
778
779 // index specified, attempt to return
780 // hardware instance. Return null if not
781 // found or not available
782 if (typeof index === "number") {
783 hardware = boards[index];
784 return hardware ? hardware : null;
785 }
786
787 // If no arg specified and hardware instances
788 // exist in the cache
789 if (boards.length) {
790 return boards[0];
791 }
792
793 // No mountable hardware
794 return null;
795};
796
797
798
799/**
800 * Board.Component
801 *
802 * Initialize a new device instance
803 *
804 * Board.Component is a |this| sensitive constructor,
805 * and must be called as:
806 *
807 * Board.Component.call( this, opts );
808 *
809 *
810 *
811 * TODO: Migrate all constructors to use this
812 * to avoid boilerplate
813 */
814
815Board.Component = function(opts, componentOpts) {
816 if (typeof opts === UNDEFINED) {
817 opts = {};
818 }
819
820 if (typeof componentOpts === UNDEFINED) {
821 componentOpts = {};
822 }
823
824 // Board specific properties
825 this.board = Board.mount(opts);
826 this.io = this.board.io;
827
828 // Component/Module instance properties
829 this.id = opts.id || Board.uid();
830 this.custom = opts.custom || {};
831
832 var originalPins;
833
834 if (typeof opts.pin === "number" || typeof opts.pin === "string") {
835 originalPins = [opts.pin];
836 } else {
837 if (Array.isArray(opts.pins)) {
838 originalPins = opts.pins.slice();
839 } else {
840 if (typeof opts.pins === "object" && opts.pins !== null) {
841
842 var pinset = opts.pins || opts.pin;
843
844 originalPins = [];
845 for (var p in pinset) {
846 originalPins.push(pinset[p]);
847 }
848 }
849 }
850 }
851
852
853 if (opts.controller) {
854
855 if (typeof opts.controller === "string") {
856 opts.controller = opts.controller.replace(/-/g, "");
857 }
858
859 if (!Expander) {
860 Expander = require("./expander");
861 }
862
863 if (Expander.hasController(opts.controller)) {
864 componentOpts = {
865 normalizePin: false,
866 requestPin: false,
867 };
868 }
869 }
870
871 componentOpts = Board.Component.initialization(componentOpts);
872
873 if (componentOpts.normalizePin) {
874 opts = Board.Pins.normalize(opts, this.board);
875 }
876
877 // var requesting = [];
878
879 if (typeof opts.pins !== UNDEFINED) {
880 this.pins = opts.pins || [];
881
882 // if (Array.isArray(this.pins)) {
883 // requesting = requesting.concat(
884 // this.pins.map(function(pin) {
885 // return {
886 // value: pin,
887 // type: "pin"
888 // };
889 // })
890 // );
891 // } else {
892 // requesting = requesting.concat(
893 // Object.keys(this.pins).map(function(key) {
894 // return {
895 // value: this.pins[key],
896 // type: "pin"
897 // };
898 // }, this)
899 // );
900 // }
901 }
902
903 if (typeof opts.pin !== UNDEFINED) {
904 this.pin = opts.pin;
905 // requesting.push({
906 // value: this.pin,
907 // type: "pin"
908 // });
909 }
910
911 // TODO: Figure out what is using this
912 /* istanbul ignore if */
913 if (typeof opts.emitter !== UNDEFINED) {
914 /* istanbul ignore next */
915 this.emitter = opts.emitter;
916 // requesting.push({
917 // value: this.emitter,
918 // type: "emitter"
919 // });
920 }
921
922 if (typeof opts.address !== UNDEFINED) {
923 this.address = opts.address;
924 // requesting.forEach(function(request) {
925 // request.address = this.address;
926 // }, this);
927 }
928
929 if (typeof opts.controller !== UNDEFINED) {
930 this.controller = opts.controller;
931 // requesting.forEach(function(request) {
932 // request.controller = this.controller;
933 // }, this);
934 }
935
936 // TODO: Figure out what is using this
937 /* istanbul ignore if */
938 if (typeof opts.bus !== UNDEFINED) {
939 /* istanbul ignore next */
940 this.bus = opts.bus;
941 // requesting.forEach(function(request) {
942 // request.bus = this.bus;
943 // }, this);
944 }
945
946 // if (componentOpts.requestPin) {
947 // // With the pins being requested for use by this component,
948 // // compare with the list of pins that are already known to be
949 // // in use by other components. If any are known to be in use,
950 // // produce a warning for the user.
951 // requesting.forEach(function(request, index) {
952 // var hasController = typeof request.controller !== UNDEFINED;
953 // var hasAddress = typeof request.address !== UNDEFINED;
954 // var isOccupied = false;
955 // var message = "";
956
957 // request.value = originalPins[index];
958
959 // if (this.board.occupied.length) {
960 // isOccupied = this.board.occupied.some(function(occupied) {
961 // var isPinOccupied = request.value === occupied.value && request.type === occupied.type;
962
963 // if (typeof occupied.controller !== UNDEFINED) {
964 // if (hasController) {
965 // return isPinOccupied && (request.controller === occupied.controller);
966 // }
967 // return false;
968 // }
969
970 // if (typeof occupied.address !== UNDEFINED) {
971 // if (hasAddress) {
972 // return isPinOccupied && (request.address === occupied.address);
973 // }
974 // return false;
975 // }
976
977 // return isPinOccupied;
978 // });
979 // }
980
981 // if (isOccupied) {
982 // message = request.type + ": " + request.value;
983
984 // if (hasController) {
985 // message += ", controller: " + request.controller;
986 // }
987
988 // if (hasAddress) {
989 // message += ", address: " + request.address;
990 // }
991
992 // this.board.warn("Component", message + " is already in use");
993 // } else {
994 // this.board.occupied.push(request);
995 // }
996 // }, this);
997 // }
998
999 this.board.register.push(this);
1000};
1001
1002Board.Component.initialization = function(opts) {
1003 var defaults = {
1004 requestPin: true,
1005 normalizePin: true
1006 };
1007
1008 return Object.assign({}, defaults, opts);
1009};
1010
1011/**
1012 * Board.Controller
1013 *
1014 * Decorate a Component with a Controller. Must be called
1015 * _AFTER_ a Controller is identified.
1016 *
1017 * Board.Controller is a |this| sensitive constructor,
1018 * and must be called as:
1019 *
1020 * Board.Controller.call( this, controller, opts );
1021 *
1022 */
1023
1024Board.Controller = function(controller, options) {
1025 var requirements = controller.requirements && controller.requirements.value;
1026
1027 if (requirements) {
1028 /* istanbul ignore else */
1029 if (requirements.options) {
1030 Object.keys(requirements.options).forEach(function(key) {
1031 /*
1032 requirements: {
1033 value: {
1034 options: {
1035 parameterName: {
1036 throws: false,
1037 message: "...blah blah blah",
1038 typeof: "number",
1039 }
1040 }
1041 }
1042 },
1043 */
1044 if (typeof options[key] === UNDEFINED ||
1045 typeof options[key] !== requirements.options[key].typeof) {
1046 if (requirements.options[key].throws) {
1047 throw new Error(requirements.options[key].message);
1048 } else {
1049 this.board.warn(this.constructor.name, requirements.options[key].message);
1050 }
1051 }
1052 }, this);
1053 }
1054 }
1055
1056 Object.defineProperties(this, controller);
1057};
1058
1059
1060
1061
1062/**
1063 * Pin Capability Signature Mapping
1064 */
1065
1066Board.Pins = Pins;
1067
1068Board.Options = Options;
1069
1070// Define a user-safe, unwritable hardware cache access
1071Object.defineProperty(Board, "cache", {
1072 get: function() {
1073 return boards;
1074 }
1075});
1076
1077/**
1078 * Board event constructor.
1079 * opts:
1080 * type - event type. eg: "read", "change", "up" etc.
1081 * target - the instance for which the event fired.
1082 * 0..* other properties
1083 */
1084Board.Event = function(event) {
1085
1086 if (typeof event === UNDEFINED) {
1087 throw new Error("Board.Event missing Event object");
1088 }
1089
1090 // default event is read
1091 this.type = event.type || "data";
1092
1093 // actual target instance
1094 this.target = event.target || null;
1095
1096 // Initialize this Board instance with
1097 // param specified properties.
1098 Object.assign(this, event);
1099};
1100
1101
1102/**
1103 * Boards or Board.Collection; Used when the program must connect to
1104 * more then one board.
1105 *
1106 * @memberof Board
1107 *
1108 * @param {Array} ports List of port objects { id: ..., port: ... }
1109 * List of id strings (initialized in order)
1110 *
1111 * @return {Boards} board object references
1112 */
1113function Boards(opts) {
1114 if (!(this instanceof Boards)) {
1115 return new Boards(opts);
1116 }
1117
1118 var ports;
1119
1120 // new Boards([ ...Array of board opts ])
1121 if (Array.isArray(opts)) {
1122 ports = opts.slice();
1123 opts = {
1124 ports: ports,
1125 };
1126 }
1127
1128 // new Boards({ ports: [ ...Array of board opts ], .... })
1129 /* istanbul ignore else */
1130 if (!Array.isArray(opts) && typeof opts === "object" && opts.ports !== undefined) {
1131 ports = opts.ports;
1132 }
1133
1134 // new Boards(non-Array?)
1135 // new Boards({ ports: non-Array? })
1136 /* istanbul ignore if */
1137 if (!Array.isArray(ports)) {
1138 throw new Error("Expected ports to be an array");
1139 }
1140
1141 if (typeof opts.debug === UNDEFINED) {
1142 opts.debug = true;
1143 }
1144
1145 if (typeof opts.repl === UNDEFINED) {
1146 opts.repl = true;
1147 }
1148
1149 var initialized = {};
1150 var noRepl = ports.some(function(port) { return port.repl === false; });
1151 var noDebug = ports.some(function(port) { return port.debug === false; });
1152
1153 this.length = ports.length;
1154 this.debug = opts.debug;
1155 this.repl = opts.repl;
1156
1157 // If any of the port definitions have
1158 // explicitly shut off debug output, bubble up
1159 // to the Boards instance
1160 /* istanbul ignore else */
1161 if (noDebug) {
1162 this.debug = false;
1163 }
1164
1165 // If any of the port definitions have
1166 // explicitly shut off the repl, bubble up
1167 // to the Boards instance
1168 /* istanbul ignore else */
1169 if (noRepl) {
1170 this.repl = false;
1171 }
1172
1173 var expecteds = ports.map(function(port, index) {
1174 var portOpts;
1175
1176 if (typeof port === "string") {
1177 portOpts = {};
1178
1179 // If the string matches a known valid port
1180 // name pattern, then assume this is what
1181 // the user code intended.
1182 if (rport.test(port)) {
1183 portOpts.port = port;
1184 } else {
1185 // Otherwise they expect Johnny-Five to figure
1186 // out what ports to use and intended this
1187 // value to be used an id
1188 portOpts.id = port;
1189 }
1190 } else {
1191 portOpts = port;
1192 }
1193
1194 // Shut off per-board repl instance creation
1195 portOpts.repl = false;
1196
1197 this[index] = initialized[portOpts.id] = new Board(portOpts);
1198
1199 // "error" event is not async, register immediately
1200 this[index].on("error", function(error) {
1201 this.emit("error", error);
1202 }.bind(this));
1203
1204 // echo the fail event from the boards
1205 this[index].on("fail", function (event) {
1206 this.emit("fail", event);
1207 }.bind(this));
1208
1209 return new Promise(function(resolve) {
1210 this[index].on("ready", function() {
1211 resolve(initialized[portOpts.id]);
1212 });
1213 }.bind(this));
1214 }, this);
1215
1216 Promise.all(expecteds).then(function(boards) {
1217 Object.assign(this, boards);
1218
1219 this.each(function(board) {
1220 board.info("Board ID: ", chalk.green(board.id));
1221 });
1222
1223 // If the Boards instance requires a REPL,
1224 // make sure it's created before calling "ready"
1225 if (this.repl) {
1226 this.repl = new Repl(
1227 Object.assign({}, initialized, {
1228 board: this
1229 })
1230 );
1231 this.repl.initialize(function() {
1232 this.emit("ready", initialized);
1233 }.bind(this));
1234 } else {
1235 // Otherwise, call ready immediately
1236 this.emit("ready", initialized);
1237 }
1238 }.bind(this));
1239}
1240
1241util.inherits(Boards, Emitter);
1242
1243Object.assign(Boards.prototype, Collection.prototype);
1244
1245Boards.prototype.byId = function(id) {
1246 for (var i = 0; i < this.length; i++) {
1247 if (this[i].id === id) {
1248 return this[i];
1249 }
1250 }
1251
1252 return null;
1253};
1254
1255Boards.prototype.log = Board.prototype.log;
1256
1257logging.specials.forEach(function(type) {
1258 /* istanbul ignore next */
1259 Boards.prototype[type] = function() {
1260 var args = [].slice.call(arguments);
1261 args.unshift(type);
1262
1263 this.log.apply(this, args);
1264 };
1265});
1266
1267/* istanbul ignore else */
1268if (IS_TEST_MODE) {
1269 Serial.purge = function() {
1270 Serial.used.length = 0;
1271 };
1272 Board.Serial = Serial;
1273
1274 Board.purge = function() {
1275 Board.Pins.normalize.clear();
1276 Repl.isActive = false;
1277 Repl.isBlocked = true;
1278 Repl.ref = null;
1279 boards.length = 0;
1280 };
1281
1282 Board.testMode = function(state) {
1283 if (!arguments.length) {
1284 return IS_TEST_MODE;
1285 } else {
1286 IS_TEST_MODE = state;
1287 }
1288 };
1289}
1290
1291// TODO: Eliminate .Array for 1.0.0
1292Board.Array = Boards;
1293Board.Collection = Boards;
1294
1295module.exports = Board;
1296
1297// References:
1298// http://arduino.cc/en/Main/arduinoBoardUno