-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
index.js
2031 lines (1881 loc) · 66.1 KB
/
index.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
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type');
const Cast = require('../../util/cast');
const formatMessage = require('format-message');
const color = require('../../util/color');
const BLE = require('../../io/ble');
const Base64Util = require('../../util/base64-util');
const MathUtil = require('../../util/math-util');
const RateLimiter = require('../../util/rateLimiter.js');
const log = require('../../util/log');
/**
* The LEGO Wireless Protocol documentation used to create this extension can be found at:
* https://lego.github.io/lego-ble-wireless-protocol-docs/index.html
*/
/**
* Icon svg to be displayed at the left edge of each extension block, encoded as a data URI.
* @type {string}
*/
// eslint-disable-next-line max-len
const iconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAMAAAC5zwKfAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACpQTFRF////fIel5ufolZ62/2YavsPS+YZOkJmy9/j53+Hk6+zs6N/b6dfO////tDhMHAAAAA50Uk5T/////////////////wBFwNzIAAAA6ElEQVR42uzX2w6DIBAEUGDVtlr//3dLaLwgiwUd2z7MJPJg5EQWiGhGcAxBggQJEiT436CIfqXJPTn3MKNYYMSDFpoAmp24OaYgvwKnFgL2zvVTCwHrMoMi+nUQLFthaNCCa0iwclLkDgYVsQp0mzxuqXgK1MRzoCLWgkPXNN2wI/q6Kvt7u/cX0HtejN8x2sXpnpb8J8D3b0Keuhh3X975M+i0xNVbg3s1TIasgK21bQyGO+s2PykaGMYbge8KrNrssvkOWDXkErB8UuBHETjoYLkKBA8ZfuDkbwVBggQJEiR4MC8BBgDTtMZLx2nFCQAAAABJRU5ErkJggg==';
/**
* Boost BLE UUIDs.
* @enum {string}
*/
const BoostBLE = {
service: '00001623-1212-efde-1623-785feabcd123',
characteristic: '00001624-1212-efde-1623-785feabcd123',
sendInterval: 100,
sendRateMax: 20
};
/**
* Boost Motor Max Power Add. Defines how much more power than the target speed
* the motors may supply to reach the target speed faster.
* Lower number == softer, slower reached target speed.
* Higher number == harder, faster reached target speed.
* @constant {number}
*/
const BoostMotorMaxPowerAdd = 10;
/**
* A time interval to wait (in milliseconds) in between battery check calls.
* @type {number}
*/
const BoostPingInterval = 5000;
/**
* The number of continuous samples the color-sensor will evaluate color from.
* @type {number}
*/
const BoostColorSampleSize = 5;
/**
* Enum for Boost sensor and actuator types.
* @readonly
* @enum {number}
*/
const BoostIO = {
MOTOR_WEDO: 0x01,
MOTOR_SYSTEM: 0x02,
BUTTON: 0x05,
LIGHT: 0x08,
VOLTAGE: 0x14,
CURRENT: 0x15,
PIEZO: 0x16,
LED: 0x17,
TILT_EXTERNAL: 0x22,
MOTION_SENSOR: 0x23,
COLOR: 0x25,
MOTOREXT: 0x26,
MOTORINT: 0x27,
TILT: 0x28
};
/**
* Enum for ids for various output command feedback types on the Boost.
* @readonly
* @enum {number}
*/
const BoostPortFeedback = {
IN_PROGRESS: 0x01,
COMPLETED: 0x02,
DISCARDED: 0x04,
IDLE: 0x08,
BUSY_OR_FULL: 0x10
};
/**
* Enum for physical Boost Ports
* @readonly
* @enum {number}
*/
const BoostPort = {
A: 55,
B: 56,
C: 1,
D: 2
};
/**
* Ids for each color sensor value used by the extension.
* @readonly
* @enum {string}
*/
const BoostColor = {
ANY: 'any',
NONE: 'none',
RED: 'red',
BLUE: 'blue',
GREEN: 'green',
YELLOW: 'yellow',
WHITE: 'white',
BLACK: 'black'
};
/**
* Enum for indices for each color sensed by the Boost vision sensor.
* @readonly
* @enum {number}
*/
const BoostColorIndex = {
[BoostColor.NONE]: 255,
[BoostColor.RED]: 9,
[BoostColor.BLUE]: 3,
[BoostColor.GREEN]: 5,
[BoostColor.YELLOW]: 7,
[BoostColor.WHITE]: 10,
[BoostColor.BLACK]: 0
};
/**
* Enum for Message Types
* @readonly
* @enum {number}
*/
const BoostMessage = {
HUB_PROPERTIES: 0x01,
HUB_ACTIONS: 0x02,
HUB_ALERTS: 0x03,
HUB_ATTACHED_IO: 0x04,
ERROR: 0x05,
PORT_INPUT_FORMAT_SETUP_SINGLE: 0x41,
PORT_INPUT_FORMAT_SETUP_COMBINED: 0x42,
PORT_INFORMATION: 0x43,
PORT_MODEINFORMATION: 0x44,
PORT_VALUE: 0x45,
PORT_VALUE_COMBINED: 0x46,
PORT_INPUT_FORMAT: 0x47,
PORT_INPUT_FORMAT_COMBINED: 0x48,
OUTPUT: 0x81,
PORT_FEEDBACK: 0x82
};
/**
* Enum for Motor Subcommands (for 0x81)
* @readonly
* @enum {number}
*/
const BoostOutputSubCommand = {
START_POWER_PAIR: 0x02,
SET_ACC_TIME: 0x05,
SET_DEC_TIME: 0x06,
START_SPEED: 0x07,
START_SPEED_PAIR: 0x08,
START_SPEED_FOR_TIME: 0x09,
START_SPEED_FOR_TIME_PAIR: 0x0A,
START_SPEED_FOR_DEGREES: 0x0B,
START_SPEED_FOR_DEGREES_PAIR: 0x0C,
GO_TO_ABS_POSITION: 0x0D,
GO_TO_ABS_POSITION_PAIR: 0x0E,
PRESET_ENCODER: 0x14,
WRITE_DIRECT_MODE_DATA: 0x51
};
/**
* Enum for Startup/Completion information for an output command.
* Startup and completion bytes must be OR'ed to be combined to a single byte.
* @readonly
* @enum {number}
*/
const BoostOutputExecution = {
// Startup information
BUFFER_IF_NECESSARY: 0x00,
EXECUTE_IMMEDIATELY: 0x10,
// Completion information
NO_ACTION: 0x00,
COMMAND_FEEDBACK: 0x01
};
/**
* Enum for Boost Motor end states
* @readonly
* @enum {number}
*/
const BoostMotorEndState = {
FLOAT: 0,
HOLD: 126,
BRAKE: 127
};
/**
* Enum for Boost Motor acceleration/deceleration profiles
* @readyonly
* @enum {number}
*/
const BoostMotorProfile = {
DO_NOT_USE: 0x00,
ACCELERATION: 0x01,
DECELERATION: 0x02
};
/**
* Enum for when Boost IO's are attached/detached
* @readonly
* @enum {number}
*/
const BoostIOEvent = {
ATTACHED: 0x01,
DETACHED: 0x00,
ATTACHED_VIRTUAL: 0x02
};
/**
* Enum for selected sensor modes.
* @enum {number}
*/
const BoostMode = {
TILT: 0, // angle (pitch/yaw)
LED: 1, // Set LED to accept RGB values
COLOR: 0, // Read indexed colors from Vision Sensor
MOTOR_SENSOR: 2, // Set motors to report their position
UNKNOWN: 0 // Anything else will use the default mode (mode 0)
};
/**
* Enum for Boost motor states.
* @param {number}
*/
const BoostMotorState = {
OFF: 0,
ON_FOREVER: 1,
ON_FOR_TIME: 2,
ON_FOR_ROTATION: 3
};
/**
* Helper function for converting a JavaScript number to an INT32-number
* @param {number} number - a number
* @return {array} - a 4-byte array of Int8-values representing an INT32-number
*/
const numberToInt32Array = function (number) {
const buffer = new ArrayBuffer(4);
const dataview = new DataView(buffer);
dataview.setInt32(0, number);
return [
dataview.getInt8(3),
dataview.getInt8(2),
dataview.getInt8(1),
dataview.getInt8(0)
];
};
/**
* Helper function for converting a regular array to a Little Endian INT32-value
* @param {Array} array - an array containing UInt8-values
* @return {number} - a number
*/
const int32ArrayToNumber = function (array) {
const i = Uint8Array.from(array);
const d = new DataView(i.buffer);
return d.getInt32(0, true);
};
/**
* Manage power, direction, position, and timers for one Boost motor.
*/
class BoostMotor {
/**
* Construct a Boost Motor instance.
* @param {Boost} parent - the Boost peripheral which owns this motor.
* @param {int} index - the zero-based index of this motor on its parent peripheral.
*/
constructor (parent, index) {
/**
* The Boost peripheral which owns this motor.
* @type {Boost}
* @private
*/
this._parent = parent;
/**
* The zero-based index of this motor on its parent peripheral.
* @type {int}
* @private
*/
this._index = index;
/**
* This motor's current direction: 1 for "this way" or -1 for "that way"
* @type {number}
* @private
*/
this._direction = 1;
/**
* This motor's current power level, in the range [0,100].
* @type {number}
* @private
*/
this._power = 50;
/**
* This motor's current relative position
* @type {number}
* @private
*/
this._position = 0;
/**
* Is this motor currently moving?
* @type {boolean}
* @private
*/
this._status = BoostMotorState.OFF;
/**
* If the motor has been turned on or is actively braking for a specific duration, this is the timeout ID for
* the end-of-action handler. Cancel this when changing plans.
* @type {Object}
* @private
*/
this._pendingDurationTimeoutId = null;
/**
* The starting time for the pending duration timeout.
* @type {number}
* @private
*/
this._pendingDurationTimeoutStartTime = null;
/**
* The delay/duration of the pending duration timeout.
* @type {number}
* @private
*/
this._pendingDurationTimeoutDelay = null;
/**
* The target position of a turn-based command.
* @type {number}
* @private
*/
this._pendingRotationDestination = null;
/**
* If the motor has been turned on run for a specific rotation, this is the function
* that will be called once Scratch VM gets a notification from the Move Hub.
* @type {Object}
* @private
*/
this._pendingRotationPromise = null;
this.turnOff = this.turnOff.bind(this);
}
/**
* @return {int} - this motor's current direction: 1 for "this way" or -1 for "that way"
*/
get direction () {
return this._direction;
}
/**
* @param {int} value - this motor's new direction: 1 for "this way" or -1 for "that way"
*/
set direction (value) {
if (value < 0) {
this._direction = -1;
} else {
this._direction = 1;
}
}
/**
* @return {int} - this motor's current power level, in the range [0,100].
*/
get power () {
return this._power;
}
/**
* @param {int} value - this motor's new power level, in the range [10,100].
*/
set power (value) {
/**
* Scale the motor power to a range between 10 and 100,
* to make sure the motors will run with something built onto them.
*/
if (value === 0) {
this._power = 0;
} else {
this._power = MathUtil.scale(value, 1, 100, 10, 100);
}
}
/**
* @return {int} - this motor's current position, in the range of [-MIN_INT32,MAX_INT32]
*/
get position () {
return this._position;
}
/**
* @param {int} value - set this motor's current position.
*/
set position (value) {
this._position = value;
}
/**
* @return {BoostMotorState} - the motor's current state.
*/
get status () {
return this._status;
}
/**
* @param {BoostMotorState} value - set this motor's state.
*/
set status (value) {
this._clearRotationState();
this._clearDurationTimeout();
this._status = value;
}
/**
* @return {number} - time, in milliseconds, of when the pending duration timeout began.
*/
get pendingDurationTimeoutStartTime () {
return this._pendingDurationTimeoutStartTime;
}
/**
* @return {number} - delay, in milliseconds, of the pending duration timeout.
*/
get pendingDurationTimeoutDelay () {
return this._pendingDurationTimeoutDelay;
}
/**
* @return {number} - target position, in degrees, of the pending rotation.
*/
get pendingRotationDestination () {
return this._pendingRotationDestination;
}
/**
* @return {Promise} - the Promise function for the pending rotation.
*/
get pendingRotationPromise () {
return this._pendingRotationPromise;
}
/**
* @param {function} func - function to resolve pending rotation Promise
*/
set pendingRotationPromise (func) {
this._pendingRotationPromise = func;
}
/**
* Turn this motor on indefinitely
* @private
*/
_turnOn () {
const cmd = this._parent.generateOutputCommand(
this._index,
BoostOutputExecution.EXECUTE_IMMEDIATELY,
BoostOutputSubCommand.START_SPEED,
[
this.power * this.direction,
MathUtil.clamp(this.power + BoostMotorMaxPowerAdd, 0, 100),
BoostMotorProfile.DO_NOT_USE
]);
this._parent.send(BoostBLE.characteristic, cmd);
}
/**
* Turn this motor on indefinitely
*/
turnOnForever () {
this.status = BoostMotorState.ON_FOREVER;
this._turnOn();
}
/**
* Turn this motor on for a specific duration.
* @param {number} milliseconds - run the motor for this long.
*/
turnOnFor (milliseconds) {
milliseconds = Math.max(0, milliseconds);
this.status = BoostMotorState.ON_FOR_TIME;
this._turnOn();
this._setNewDurationTimeout(this.turnOff, milliseconds);
}
/**
* Turn this motor on for a specific rotation in degrees.
* @param {number} degrees - run the motor for this amount of degrees.
* @param {number} direction - rotate in this direction
*/
turnOnForDegrees (degrees, direction) {
degrees = Math.max(0, degrees);
const cmd = this._parent.generateOutputCommand(
this._index,
(BoostOutputExecution.EXECUTE_IMMEDIATELY ^ BoostOutputExecution.COMMAND_FEEDBACK),
BoostOutputSubCommand.START_SPEED_FOR_DEGREES,
[
...numberToInt32Array(degrees),
this.power * this.direction * direction,
MathUtil.clamp(this.power + BoostMotorMaxPowerAdd, 0, 100),
BoostMotorEndState.BRAKE,
BoostMotorProfile.DO_NOT_USE
]
);
this.status = BoostMotorState.ON_FOR_ROTATION;
this._pendingRotationDestination = this.position + (degrees * this.direction * direction);
this._parent.send(BoostBLE.characteristic, cmd);
}
/**
* Turn this motor off.
* @param {boolean} [useLimiter=true] - if true, use the rate limiter
*/
turnOff (useLimiter = true) {
const cmd = this._parent.generateOutputCommand(
this._index,
BoostOutputExecution.EXECUTE_IMMEDIATELY ^ BoostOutputExecution.COMMAND_FEEDBACK,
BoostOutputSubCommand.START_SPEED,
[
BoostMotorEndState.FLOAT,
BoostMotorEndState.FLOAT,
BoostMotorProfile.DO_NOT_USE
]
);
this.status = BoostMotorState.OFF;
this._parent.send(BoostBLE.characteristic, cmd, useLimiter);
}
/**
* Clear the motor action timeout, if any. Safe to call even when there is no pending timeout.
* @private
*/
_clearDurationTimeout () {
if (this._pendingDurationTimeoutId !== null) {
clearTimeout(this._pendingDurationTimeoutId);
this._pendingDurationTimeoutId = null;
this._pendingDurationTimeoutStartTime = null;
this._pendingDurationTimeoutDelay = null;
}
}
/**
* Set a new motor action timeout, after clearing an existing one if necessary.
* @param {Function} callback - to be called at the end of the timeout.
* @param {int} delay - wait this many milliseconds before calling the callback.
* @private
*/
_setNewDurationTimeout (callback, delay) {
this._clearDurationTimeout();
const timeoutID = setTimeout(() => {
if (this._pendingDurationTimeoutId === timeoutID) {
this._pendingDurationTimeoutId = null;
this._pendingDurationTimeoutStartTime = null;
this._pendingDurationTimeoutDelay = null;
}
callback();
}, delay);
this._pendingDurationTimeoutId = timeoutID;
this._pendingDurationTimeoutStartTime = Date.now();
this._pendingDurationTimeoutDelay = delay;
}
/**
* Clear the motor states related to rotation-based commands, if any.
* Safe to call even when there is no pending promise function.
* @private
*/
_clearRotationState () {
if (this._pendingRotationPromise !== null) {
this._pendingRotationPromise();
this._pendingRotationPromise = null;
}
this._pendingRotationDestination = null;
}
}
/**
* Manage communication with a Boost peripheral over a Bluetooth Low Energy client socket.
*/
class Boost {
constructor (runtime, extensionId) {
/**
* The Scratch 3.0 runtime used to trigger the green flag button.
* @type {Runtime}
* @private
*/
this._runtime = runtime;
this._runtime.on('PROJECT_STOP_ALL', this.stopAll.bind(this));
/**
* The id of the extension this peripheral belongs to.
*/
this._extensionId = extensionId;
/**
* A list of the ids of the physical or virtual sensors.
* @type {string[]}
* @private
*/
this._ports = [];
/**
* A list of motors registered by the Boost hardware.
* @type {BoostMotor[]}
* @private
*/
this._motors = [];
/**
* The most recently received value for each sensor.
* @type {Object.<string, number>}
* @private
*/
this._sensors = {
tiltX: 0,
tiltY: 0,
color: BoostColor.NONE,
previousColor: BoostColor.NONE
};
/**
* An array of values from the Boost Vision Sensor.
* @type {Array}
* @private
*/
this._colorSamples = [];
/**
* The Bluetooth connection socket for reading/writing peripheral data.
* @type {BLE}
* @private
*/
this._ble = null;
this._runtime.registerPeripheralExtension(extensionId, this);
/**
* A rate limiter utility, to help limit the rate at which we send BLE messages
* over the socket to Scratch Link to a maximum number of sends per second.
* @type {RateLimiter}
* @private
*/
this._rateLimiter = new RateLimiter(BoostBLE.sendRateMax);
/**
* An interval id for the battery check interval.
* @type {number}
* @private
*/
this._pingDeviceId = null;
this.reset = this.reset.bind(this);
this._onConnect = this._onConnect.bind(this);
this._onMessage = this._onMessage.bind(this);
this._pingDevice = this._pingDevice.bind(this);
}
/**
* @return {number} - the latest value received for the tilt sensor's tilt about the X axis.
*/
get tiltX () {
return this._sensors.tiltX;
}
/**
* @return {number} - the latest value received for the tilt sensor's tilt about the Y axis.
*/
get tiltY () {
return this._sensors.tiltY;
}
/**
* @return {number} - the latest color value received from the vision sensor.
*/
get color () {
return this._sensors.color;
}
/**
* @return {number} - the previous color value received from the vision sensor.
*/
get previousColor () {
return this._sensors.previousColor;
}
/**
* Look up the color id for an index received from the vision sensor.
* @param {number} index - the color index to look up.
* @return {BoostColor} the color id for this index.
*/
boostColorForIndex (index) {
const colorForIndex = Object.keys(BoostColorIndex).find(key => BoostColorIndex[key] === index);
return colorForIndex || BoostColor.NONE;
}
/**
* Access a particular motor on this peripheral.
* @param {int} index - the index of the desired motor.
* @return {BoostMotor} - the BoostMotor instance, if any, at that index.
*/
motor (index) {
return this._motors[index];
}
/**
* Stop all the motors that are currently running.
*/
stopAllMotors () {
this._motors.forEach(motor => {
if (motor) {
// Send the motor off command without using the rate limiter.
// This allows the stop button to stop motors even if we are
// otherwise flooded with commands.
motor.turnOff(false);
}
});
}
/**
* Set the Boost peripheral's LED to a specific color.
* @param {int} inputRGB - a 24-bit RGB color in 0xRRGGBB format.
* @return {Promise} - a promise of the completion of the set led send operation.
*/
setLED (inputRGB) {
const rgb = [
(inputRGB >> 16) & 0x000000FF,
(inputRGB >> 8) & 0x000000FF,
(inputRGB) & 0x000000FF
];
const cmd = this.generateOutputCommand(
this._ports.indexOf(BoostIO.LED),
BoostOutputExecution.EXECUTE_IMMEDIATELY ^ BoostOutputExecution.COMMAND_FEEDBACK,
BoostOutputSubCommand.WRITE_DIRECT_MODE_DATA,
[BoostMode.LED,
...rgb]
);
return this.send(BoostBLE.characteristic, cmd);
}
/**
* Sets the input mode of the LED to RGB.
* @return {Promise} - a promise returned by the send operation.
*/
setLEDMode () {
const cmd = this.generateInputCommand(
this._ports.indexOf(BoostIO.LED),
BoostMode.LED,
0,
false
);
return this.send(BoostBLE.characteristic, cmd);
}
/**
* Stop the motors on the Boost peripheral.
*/
stopAll () {
if (!this.isConnected()) return;
this.stopAllMotors();
}
/**
* Called by the runtime when user wants to scan for a Boost peripheral.
*/
scan () {
if (this._ble) {
this._ble.disconnect();
}
this._ble = new BLE(this._runtime, this._extensionId, {
filters: [{
services: [BoostBLE.service]/* ,
manufacturerData: {
0: {
dataPrefix: [0x97, 0x03, 0x00, 0x40],
mask: [0xFF, 0xFF, 0, 0xFF]
}
} commented out until feature is enabled in scratch-link */
}],
optionalServices: []
}, this._onConnect, this.reset);
}
/**
* Called by the runtime when user wants to connect to a certain Boost peripheral.
* @param {number} id - the id of the peripheral to connect to.
*/
connect (id) {
if (this._ble) {
this._ble.connectPeripheral(id);
}
}
/**
* Disconnects from the current BLE socket and resets state.
*/
disconnect () {
if (this._ble) {
this._ble.disconnect();
}
this.reset();
}
/**
* Reset all the state and timeout/interval ids.
*/
reset () {
this._ports = [];
this._motors = [];
this._sensors = {
tiltX: 0,
tiltY: 0,
color: BoostColor.NONE,
previousColor: BoostColor.NONE
};
if (this._pingDeviceId) {
window.clearInterval(this._pingDeviceId);
this._pingDeviceId = null;
}
}
/**
* Called by the runtime to detect whether the Boost peripheral is connected.
* @return {boolean} - the connected state.
*/
isConnected () {
let connected = false;
if (this._ble) {
connected = this._ble.isConnected();
}
return connected;
}
/**
* Write a message to the Boost peripheral BLE socket.
* @param {number} uuid - the UUID of the characteristic to write to
* @param {Array} message - the message to write.
* @param {boolean} [useLimiter=true] - if true, use the rate limiter
* @return {Promise} - a promise result of the write operation
*/
send (uuid, message, useLimiter = true) {
if (!this.isConnected()) return Promise.resolve();
if (useLimiter) {
if (!this._rateLimiter.okayToSend()) return Promise.resolve();
}
return this._ble.write(
BoostBLE.service,
uuid,
Base64Util.uint8ArrayToBase64(message),
'base64'
);
}
/**
* Generate a Boost 'Output Command' in the byte array format
* (COMMON HEADER, PORT ID, EXECUTION BYTE, SUBCOMMAND ID, PAYLOAD).
*
* Payload is accepted as an array since these vary across different subcommands.
*
* @param {number} portID - the port (Connect ID) to send a command to.
* @param {number} execution - Byte containing startup/completion information
* @param {number} subCommand - the id of the subcommand byte.
* @param {array} payload - the list of bytes to send as subcommand payload
* @return {array} - a generated output command.
*/
generateOutputCommand (portID, execution, subCommand, payload) {
const hubID = 0x00;
const command = [hubID, BoostMessage.OUTPUT, portID, execution, subCommand, ...payload];
command.unshift(command.length + 1); // Prepend payload with length byte;
return command;
}
/**
* Generate a Boost 'Input Command' in the byte array format
* (COMMAND ID, COMMAND TYPE, CONNECT ID, TYPE ID, MODE, DELTA INTERVAL (4 BYTES),
* UNIT, NOTIFICATIONS ENABLED).
*
* This sends a command to the Boost that sets that input format
* of the specified inputs and sets value change notifications.
*
* @param {number} portID - the port (Connect ID) to send a command to.
* @param {number} mode - the mode of the input sensor.
* @param {number} delta - the delta change needed to trigger notification.
* @param {boolean} enableNotifications - whether to enable notifications.
* @return {array} - a generated input command.
*/
generateInputCommand (portID, mode, delta, enableNotifications) {
const command = [
0x00, // Hub ID
BoostMessage.PORT_INPUT_FORMAT_SETUP_SINGLE,
portID,
mode
].concat(numberToInt32Array(delta)).concat([
enableNotifications
]);
command.unshift(command.length + 1); // Prepend payload with length byte;
return command;
}
/**
* Starts reading data from peripheral after BLE has connected.
* @private
*/
_onConnect () {
this._ble.startNotifications(
BoostBLE.service,
BoostBLE.characteristic,
this._onMessage
);
this._pingDeviceId = window.setInterval(this._pingDevice, BoostPingInterval);
}
/**
* Process the sensor data from the incoming BLE characteristic.
* @param {object} base64 - the incoming BLE data.
* @private
*/
_onMessage (base64) {
const data = Base64Util.base64ToUint8Array(base64);
/**
* First three bytes are the common header:
* 0: Length of message
* 1: Hub ID (always 0x00 at the moment, unused)
* 2: Message Type
* 3: Port ID
* We base our switch-case on Message Type
*/
const messageType = data[2];
const portID = data[3];
switch (messageType) {
case BoostMessage.HUB_ATTACHED_IO: { // IO Attach/Detach events
const event = data[4];
const typeId = data[5];
switch (event) {
case BoostIOEvent.ATTACHED:
this._registerSensorOrMotor(portID, typeId);
break;
case BoostIOEvent.DETACHED:
this._clearPort(portID);
break;
case BoostIOEvent.ATTACHED_VIRTUAL:
default:
}
break;
}
case BoostMessage.PORT_VALUE: {
const type = this._ports[portID];
switch (type) {
case BoostIO.TILT:
this._sensors.tiltX = data[4];
this._sensors.tiltY = data[5];
break;
case BoostIO.COLOR:
this._colorSamples.unshift(data[4]);
if (this._colorSamples.length > BoostColorSampleSize) {
this._colorSamples.pop();
if (this._colorSamples.every((v, i, arr) => v === arr[0])) {
this._sensors.previousColor = this._sensors.color;