UNPKG

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