-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
Copy pathcommon-hid-packet-parser.js
2241 lines (2146 loc) · 88.2 KB
/
common-hid-packet-parser.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
/* global controller */
/**
* Common HID script debugging function. Just to get logging with 'HID' prefix.
* @deprecated Use console.log instead
* @param {any} message Message to be printed on controller debug console output
*/
this.HIDDebug = function(message) {
console.log(`HID ${message}`);
};
/**
* creates a `DataView` from any ArrayBuffer, TypedArray
* or plain Array (clamped to 8-bit width).
* @param {number[] | ArrayBuffer | Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array } bufferLike Object that can be represented as a sequence of bytes
* @returns {DataView} dataview over the bufferLike object
*/
const createDataView = function(bufferLike) {
return new DataView((() => {
if (Array.isArray(bufferLike)) {
return new Uint8ClampedArray(bufferLike).buffer;
} else if (ArrayBuffer.isView(bufferLike)) {
return bufferLike.buffer;
} else {
return bufferLike;
}
})());
};
/**
* Callback function to call when, the packet represents an HID InputReport, and new data for this
* InputReport are received. If a packet callback is defined and the data for the InputReport are
* received, the complete report data are sent to the callback function after field values are
* parsed, without calling any packet field parsing functions.
* @callback packetCallback
* @param {HIDPacket} packet The packet that represents the InputReport
* @param {Object.<string, packetField | bitObject>} changed_data The data received from the device
*/
/**
* Callback function to call when, the value of a modifier control changed
* @callback modifierCallback
* @param {boolean} Value of the modifier control
*/
/**
* Callback function to call when, data for specified filed in the packet is updated.
* @callback fieldChangeCallback
* @param {packetField|bitObject} Object that describes a field/bit inside of a packet, which can often
* mapped to a Mixxx control.
*/
/**
* Callback function, which will be called every time, the value of the connected control changes.
* @callback controlCallback
* @param {number} value New value of the control
* @param {string} group Mixxx control group name e.g. "[Channel1]"
* @param {string} name Mixxx control name "pregain"
* @returns {any} Value
*/
/**
* In almost every case, a HID controller sends data values with input fields which are not directly
* suitable for Mixxx control values. To solve this issue, HIDController contains function to scale
* the input value to suitable range automatically before calling any field processing functions.
* Scalers can be registered with HIDController.setScaler.
*
* The ScallingCallback function can also have a boolean property .useSetParameter, if:
* - 'false' or 'undefined', engine.setValue is used
* - 'true' engine.setParameter is used
* @callback scalingCallback
* @param {string} group Control group name e.g. "[Channel1]"
* @param {string} name Control name "pregain"
* @param {number} value Value to be scaled
* @returns {number} Scaled value
*/
/**
* Callback function to call when, jog wheel scratching got enabled or disabled by
* the button with the special name 'jog_touch'
* @callback scratchingCallback
* @param {boolean} isScratchEnabled True, when button 'jog_touch' is active
*/
/**
* @typedef packetField
* @type {Object}
* @property {HIDPacket} packet
* @property {string} id Group and control name separated by a dot
* @property {string} group
* @property {string} name
* @property {string} mapped_group Mapped group, must be a valid Mixxx control group name e.g. "[Channel1]"
* @property {string} mapped_name Name of mapped control, must be a valid Mixxx control name "vu_meter"
* @property {controlCallback} mapped_callback
* @property {string} pack Control packing format for unpack(), one of b/B, h/H, i/I
* @property {number} offset Position of the first byte in the packet in bytes (first byte is 0)
* @property {number} end_offset Position of the last byte in the packet in bytes ({@link packetField.offset} + packet size)
* @property {number} bitmask
* @property {boolean} isEncoder
* @property {fieldChangeCallback} callback
* @property {boolean} soft_takeover
* @property {boolean} ignored
* @property {fieldChangeCallback} auto_repeat
* @property {number} auto_repeat_interval
* @property {number} min
* @property {number} max
* @property {('bitvector'|'control'|'output')} type Must be either:
* - 'bitvector' If value is of type HIDBitVector
* - 'control' If value is a number
* - 'output'
* @property {HIDBitVector|boolean|number} value
* @property {number} delta
* @property {number} mindelta
* @property {number} toggle
*/
/**
* @typedef bitObject
* @type {Object}
* @property {HIDPacket} packet
* @property {string} id Group and control name separated by a dot
* @property {string} group
* @property {string} name
* @property {string} mapped_group Mapped group, must be a valid Mixxx control group name e.g. "[Channel1]"
* @property {string} mapped_name Name of mapped control, must be a valid Mixxx control name "cue_indicator"
* @property {controlCallback} mapped_callback
* @property {number} bitmask
* @property {number} bit_offset
* @property {fieldChangeCallback} callback
* @property {fieldChangeCallback} auto_repeat
* @property {number} auto_repeat_interval
* @property {('button'|'output')} type Must be either:
* - 'button'
* - 'output'
* @property {number} value
* @property {number} toggle
*/
/**
* HID Bit Vector Class
*
* Collection of bits in one parsed packet field. These objects are
* created by HIDPacket addControl and addOutput and should not be
* created manually.
*/
// @ts-ignore Same identifier for class and instance needed for backward compatibility
class HIDBitVector {
constructor() {
/**
* Number of bitObjects in bits array
* @type {number}
*/
this.size = 0;
/**
* Object of bitObjects, referred by a string of group and control name separated by a dot
* @type {Object.<string, bitObject>}
*/
this.bits = {};
}
/**
* Get the index of the least significant bit that is 1 in `bitmask`
* @param {number} bitmask A bitwise mask of up to 32 bit. All bits set to'1' in this mask are
* considered.
* @returns {number} Index of the least significant bit that is 1 in `bitmask`
*/
getOffset(bitmask) {
bitmask >>>= 0; // ensures coercion to Uint32
// The previous implementation should have returned 32 for an empty bitmask, instead it
// returned 0
if (bitmask === 0) {
return 0;
} // skipping this step would make it return -1
bitmask &= -bitmask; // equivalent to `bitmask = bitmask & (~bitmask + 1)`
return 31 - Math.clz32(bitmask);
}
/**
* Add a control bitmask to the HIDBitVector
* @param {string} group Control group name e.g. "[Channel1]"
* @param {string} name Control name e.g. "play"
* @param {number} bitmask A bitwise mask of up to 32 bit. All bits set to'1' in this mask are
* considered.
*/
addBitMask(group, name, bitmask) {
/** @type {bitObject} */
const bit = {};
bit.type = "button";
bit.packet = undefined;
bit.id = `${group}.${name}`;
bit.group = group;
bit.name = name;
bit.mapped_group = undefined;
bit.mapped_name = undefined;
bit.bitmask = bitmask;
bit.bit_offset = this.getOffset(bitmask);
bit.callback = undefined;
bit.value = undefined;
bit.auto_repeat = undefined;
bit.auto_repeat_interval = undefined;
this.bits[bit.id] = bit;
}
/**
* Add an output control bitmask to the HIDBitVector
* @param {string} group Control group name e.g. "[Channel1]"
* @param {string} name Control name e.g. "play"
* @param {number} bitmask A bitwise mask of up to 32 bit. All bits set to'1' in this mask are
* considered.
*/
addOutputMask(group, name, bitmask) {
/** @type {bitObject} */
const bit = {};
bit.type = "output";
bit.packet = undefined;
bit.id = `${group}.${name}`;
bit.group = group;
bit.name = name;
bit.mapped_group = undefined;
bit.mapped_name = undefined;
bit.bitmask = bitmask;
bit.bit_offset = this.getOffset(bitmask);
bit.callback = undefined;
bit.value = undefined;
bit.toggle = undefined;
this.bits[bit.id] = bit;
}
}
// Add class HIDBitVector to the Global JavaScript object
// @ts-ignore Same identifier for class and instance needed for backward compatibility
this.HIDBitVector = HIDBitVector;
/**
* HID Modifiers object
*
* e.g. a shift button can be defined as modifier for the behavior of other controls.
*
* Wraps all defined modifiers to one object with uniform API.
* Don't call directly, this is available as HIDController.modifiers
*/
// @ts-ignore Same identifier for class and instance needed for backward compatibility
class HIDModifierList {
constructor() {
/**
* Actual value of the modifier
* @type {Object.<string, boolean>}
*/
this.modifiers = Object();
/**
* Function to be called after modifier value changes
* @type {Object.<string, modifierCallback>}
*/
this.callbacks = Object();
}
/**
* Add a new modifier to controller.
* @param {string} name Name of modifier
*/
add(name) {
if (name in this.modifiers) {
console.warn(`HIDModifierList.add - Modifier already defined: ${name}`);
return;
}
this.modifiers[name] = undefined;
}
/**
* Set modifier value
* @param {string} name Name of modifier
* @param {boolean} value Value to be set
*/
set(name, value) {
if (!(name in this.modifiers)) {
console.error(`HIDModifierList.set - Unknown modifier: ${name}`);
return;
}
this.modifiers[name] = value;
if (name in this.callbacks) {
const callback = this.callbacks[name];
callback(value);
}
}
/**
* Get modifier value
* @param {string} name Name of modifier
* @returns {boolean} Value of modifier
*/
get(name) {
if (!(name in this.modifiers)) {
console.error(`HIDModifierList.get - Unknown modifier: ${name}`);
return false;
}
return this.modifiers[name];
}
/**
* Set modifier callback function
* @param {string} name Name of reference in HIDModifierList
* @param {modifierCallback} callback Function to be called after modifier value changes
*/
setCallback(name, callback) {
if (!(name in this.modifiers)) {
console.error(`HIDModifierList.setCallback - Unknown modifier: ${name}`);
return;
}
this.callbacks[name] = callback;
}
}
// Add class HIDModifierList to the Global JavaScript object
// @ts-ignore Same identifier for class and instance needed for backward compatibility
this.HIDModifierList = HIDModifierList;
/**
* HID Packet object
*
* An HIDPacket represents one HID report of type InputReport or OutputReport (FeatureReports are
* currently not supported)
*
* Each HIDPacket must be registered to HIDController.
*/
// @ts-ignore Same identifier for class and instance needed for backward compatibility
class HIDPacket {
/**
* @param {string} name Name of packet (it makes sense to refer the HID report type and HID
* ReportID here e.g. 'InputReport_0x02' or 'OutputReport_0x81')
* @param {number} reportId ReportID of the packet. If the device does not use ReportIDs this
* must be 0. [default = 0]
* @param {packetCallback} callback function to call when the packet type represents an InputReport,
* and a new report is received. If packet callback is set, the packet is not parsed by delta
* functions. Note, that a callback is not meaningful for output packets.
* @param {number[]} header (optional) List of bytes to match from beginning of packet.
* Do NOT put the report ID in this - use the reportId parameter instead.
*/
constructor(name, reportId = 0, callback = undefined, header = []) {
/**
* Name of packet
* @type {string}
*/
this.name = name;
/**
* ReportID of the packet. If the device does not use ReportIDs this must be 0.
* @type {number}
*/
this.reportId = reportId;
/**
* Function to call when the packet type represents an InputReport, and a new report is received.
* @type {packetCallback}
*/
this.callback = callback;
/**
* List of bytes to match from beginning of packet
* @type {number[]}
*/
this.header = header;
/**
* Object of groups, referred by the group string
* @type {Object.<string, Object.<string, any>>}
*/
this.groups = {};
/**
* Length of packet in bytes
* @type {number}
*/
this.length = this.header.length;
/**
* Size of the 'pack' types in bytes
* @type {Object.<string, number>}
*/
this.packSizes = {b: 1, B: 1, h: 2, H: 2, i: 4, I: 4};
this.signedPackFormats = ["b", "h", "i"];
}
/**
* Pack a field value to the packet.
* Can only pack bits and byte values, patches welcome.
* @todo Implement multi byte bit vector outputs
* @param {Uint8Array} data Data to be send as OutputReport to the device
* @param {packetField} field Object that describes a field inside of a packet, which can often
* mapped to a Mixxx control.
*/
pack(data, field) {
if (!(field.pack in this.packSizes)) {
console.error(`HIDPacket.pack - Parsing packed value: invalid pack format ${field.pack}`);
return;
}
if (field.type === "bitvector") {
const bitVector = /** @type {HIDBitVector} */ (field.value);
if (this.packSizes[field.pack] > 1) {
console.error("HIDPacket.pack - Packing multibyte bit vectors not yet supported");
return;
}
HIDController.fastForIn(bitVector.bits, (bit) => {
data[field.offset] |= bitVector.bits[bit].value;
}
);
return;
}
const value = Number((field.value !== undefined) ? field.value : 0);
if (value < field.min || value > field.max) {
console.error(`HIDPacket.pack - ${field.id} packed value out of range: ${value}`);
return;
}
const dataView = createDataView(data);
switch (field.pack) {
case "b":
dataView.setInt8(field.offset, value);
break;
case "B":
dataView.setUint8(field.offset, value);
break;
case "h":
dataView.setInt16(field.offset, value, true);
break;
case "H":
dataView.setUint16(field.offset, value, true);
break;
case "i":
dataView.setInt32(field.offset, value, true);
break;
case "I":
dataView.setUint32(field.offset, value, true);
break;
default:
// Impossible, because range checked at beginning of the function
}
}
/**
* Parse and return field value matching the 'pack' field from field attributes.
* Valid field packing types are:
* - b signed byte
* - B unsigned byte
* - h signed short
* - H unsigned short
* - i signed integer
* - I unsigned integer
* @param {number[] | ArrayBuffer | Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array} data Data received as InputReport from the device
* @param {packetField} field Object that describes a field inside of a packet, which can often
* mapped to a Mixxx control.
* @returns {number} Value for the field in data, represented according the fields packing type
*/
unpack(data, field) {
const dataView = createDataView(data);
switch (field.pack) {
case "b":
return dataView.getInt8(field.offset);
case "B":
return dataView.getUint8(field.offset);
case "h":
return dataView.getInt16(field.offset, true);
case "H":
return dataView.getUint16(field.offset, true);
case "i":
return dataView.getInt32(field.offset, true);
case "I":
return dataView.getUint32(field.offset, true);
default:
console.error(`HIDPacket.unpack - Parsing packed value: invalid pack format ${field.pack}`);
return undefined;
}
}
/**
* Find HID packet group matching name.
* Create group if create is true
* @param {string} name Name of the group
* @param {boolean} [create] If true, group will be created
@returns {any} Group Returns group or undefined, when group is not existing and create is set
to false
*/
getGroup(name, create) {
if (this.groups === undefined) {
this.groups = {};
}
if (name in this.groups) {
return this.groups[name];
}
if (!create) {
return undefined;
}
this.groups[name] = {};
return this.groups[name];
}
/**
* Lookup HID packet field matching given offset and pack type
* @param {number} offset The field's offset from the start of the packet in bytes:
* - For HID devices which don't use ReportIDs, the data bytes starts at
* position 0
* - For HID devices which use ReportIDs to enumerate the reports, the
* data bytes starts at position 1
* @param {string} pack Is one of the field packing types:
* - b signed byte (Int8)
* - B unsigned byte (Uint8)
* - h signed short (Int16 Little-Endian)
* - H unsigned short (Uint16 Little-Endian)
* - i signed integer (Int32 Little-Endian)
* - I unsigned integer (Uint32 Little-Endian)
* @returns {packetField} Returns matching field or undefined if no matching field can be found.
*/
getFieldByOffset(offset, pack) {
if (!(pack in this.packSizes)) {
console.error(`HIDPacket.getFieldByOffset - Unknown pack string ${pack}`);
return undefined;
}
const end_offset = offset + this.packSizes[pack];
for (const group_name in this.groups) {
const group = this.groups[group_name];
for (const field_id in group) {
/** @type {packetField} */
const field = group[field_id];
// Same field offset
if (field.offset === offset) {
return field;
}
// 7-8 8-9
// Offset for smaller packet inside multibyte field
if (field.offset < offset && field.end_offset >= end_offset) {
return field;
}
// Packet offset starts inside field, may overflow
if (field.offset < offset && field.end_offset > offset) {
return field;
}
// Packet start before field, ends or overflows field
if (field.offset > offset && field.offset < end_offset) {
return field;
}
}
}
return undefined;
}
/**
* Return a field by group and name from the packet,
* Returns undefined if field could not be found
* @param {string} group Control group name e.g. "[Channel1]"
* @param {string} name Control name e.g. "play"
* @returns {packetField} Field
*/
getField(group, name) {
const field_id = `${group}.${name}`;
if (!(group in this.groups)) {
console.error(`HIDPacket.getField - Packet ${this.name} group not found ${group}`);
return undefined;
}
const control_group1 = this.groups[group];
if (field_id in control_group1) {
return control_group1[field_id];
}
// Lookup for bit fields in bitvector matching field name
for (const group_name in this.groups) {
const control_group2 = this.groups[group_name];
for (const field_name in control_group2) {
const field = control_group2[field_name];
if (field === undefined || field.type !== "bitvector") {
continue;
}
for (const bit_name in field.value.bits) {
const bit = field.value.bits[bit_name];
if (bit.id === field_id) {
return field;
}
}
}
}
// Field not found
return undefined;
}
/**
* Return reference to a bit in a bitvector field
* @param {string} group Control group name e.g. "[Channel1]"
* @param {string} name Control name e.g. "play"
* @returns {bitObject} Reference to a bit in a bitvector field
*/
lookupBit(group, name) {
const field = this.getField(group, name);
if (field === undefined) {
console.error(`HIDPacket.lookupBit - Bitvector match not found: ${group}.${name}`);
return undefined;
}
if (field.type !== "bitvector") {
console.error(`HIDPacket.lookupBit - Control doesn't refer a field of type bitvector: ${group}.${name}`);
return undefined;
}
const bitVector = /** @type {HIDBitVector} */ (field.value);
const bit_id = `${group}.${name}`;
// Fast loop implementation over bitvector.bits object
const bitVectorKeyArr = Object.keys(bitVector.bits);
let bitVectorKeyIdx = bitVectorKeyArr.length;
while (bitVectorKeyIdx--) {
const bit = bitVector.bits[bitVectorKeyArr[bitVectorKeyIdx]];
if (bit.id === bit_id) {
return bit;
}
}
console.error("HIDPacket.lookupBit - BUG: bit not found after successful field lookup");
return undefined;
}
/**
* Remove a control registered. Normally not needed
* @param {string} group Control group name e.g. "[Channel1]"
* @param {string} name Control name e.g. "play"
*/
removeControl(group, name) {
const control_group = this.getGroup(group);
if (!(name in control_group)) {
console.warn(`HIDPacket.removeControl - Field not in control group ${group}: ${name}`);
return;
}
delete control_group[name];
}
/**
* Register a numeric value to parse from input packet
*
* 'group' and 'name' form the ID of the field, if it matches a valid Mixxx control name,
* the system attempts to attach it directly to the correct field.
* @param {string} group Control group name e.g. "[Channel1]"
* @param {string} name Control name e.g. "play"
* @param {number} offset The field's offset from the start of the packet in bytes:
* - For HID devices which don't use ReportIDs, the data bytes starts at
* position 0
* - For HID devices which use ReportIDs to enumerate the reports, the
* data bytes starts at position 1
* @param {string} pack Is one of the field packing types:
* - b signed byte (Int8)
* - B unsigned byte (Uint8)
* - h signed short (Int16 Little-Endian)
* - H unsigned short (Uint16 Little-Endian)
* - i signed integer (Int32 Little-Endian)
* - I unsigned integer (Uint32 Little-Endian)
* @param {number} [bitmask] A bitwise mask of up to 32 bit. All bits set to'1' in this mask are
* considered.
* Note: For controls that use full bytes (8bit, 16bit, ...), you can set this to
* undefined NOTE: Parsing bitmask with multiple bits is not supported yet.
* @param {boolean} [isEncoder] indicates if this is an encoder which should be wrapped and delta
* reported
* @param {fieldChangeCallback} [callback] Callback function for the control
*/
addControl(group, name, offset, pack, bitmask, isEncoder, callback) {
const control_group = this.getGroup(group, true);
if (control_group === undefined) {
console.error(`HIDPacket.addControl - Creating HID packet group ${group}`);
return;
}
if (!(pack in this.packSizes)) {
console.error(`HIDPacket.addControl - Unknown pack value ${pack}`);
return;
}
const fieldByOffset = this.getFieldByOffset(offset, pack);
if (fieldByOffset !== undefined) {
if (bitmask === undefined) {
console.error(`HIDPacket.addControl - Registering offset ${offset} pack ${pack}`);
console.error(`HIDPacket.addControl - Trying to overwrite non-bitmask control ${group} ${name}`);
return;
}
if (fieldByOffset.type !== "bitvector") {
console.error(`HIDPacket.addControl - Field is not of type bitvector: ${group}.${name}`);
return;
} else {
const bitVector = /** @type {HIDBitVector} */ (fieldByOffset.value);
bitVector.addBitMask(group, name, bitmask);
if (callback !== undefined) {
if (typeof callback !== "function") {
console.error(
`HIDPacket.addControl - Callback provided for ${group}.${name} is not a function.`);
return;
}
this.setCallback(group, name, callback);
}
return;
}
}
/** @type {packetField} */
const field = {};
field.packet = undefined;
field.id = `${group}.${name}`;
field.group = group;
field.name = name;
field.mapped_group = undefined;
field.mapped_name = undefined;
field.pack = pack;
field.offset = offset;
field.end_offset = offset + this.packSizes[field.pack];
field.bitmask = bitmask;
field.isEncoder = isEncoder;
field.callback = undefined;
field.soft_takeover = false;
field.ignored = false;
field.auto_repeat = undefined;
field.auto_repeat_interval = undefined;
const packet_max_value = Math.pow(2, this.packSizes[field.pack] * 8) - 1;
const signed = this.signedPackFormats.includes(field.pack);
if (signed) {
field.min = 0 - ((packet_max_value + 1) / 2) + 1;
field.max = ((packet_max_value + 1) / 2) - 1;
} else {
field.min = 0;
field.max = packet_max_value;
}
if (bitmask === undefined || bitmask === packet_max_value) {
field.type = "control";
field.value = undefined;
field.delta = 0;
field.mindelta = 0;
} else {
// bitmask is only defined for fields which are not expected to handle all bits in the
// control field. For fields with bitmasks, you can define same offset and pack multiple
// times with different bitmask values to get for example all 8 bits of a buttons state
// byte to different control fields in addControl input packet command. Masking multiple
// bits should work but has not been as widely tested.
const signed = this.signedPackFormats.includes(field.pack);
if (signed) {
console.error("HIDPacket.addControl - Registering bitvector: signed fields not supported");
return;
}
// Create a new bitvector field and add the bit to that
// TODO - accept controls with bitmask < packet_max_value
const field_name = `bitvector_${offset}`;
field.type = "bitvector";
field.name = field_name;
field.id = `${group}.${field_name}`;
const bitvector = new HIDBitVector();
bitvector.size = field.max;
bitvector.addBitMask(group, name, bitmask);
field.value = bitvector;
field.delta = undefined;
field.soft_takeover = undefined;
field.mindelta = undefined;
}
// Add the new field to the packet
control_group[field.id] = field;
if (callback !== undefined) {
if (typeof callback !== "function") {
console.error(
`HIDPacket.addControl - Callback provided for ${group}.${name} is not a function.`);
return;
}
this.setCallback(group, name, callback);
}
}
/**
* Register a Output control field or Output control bit to output packet
* Output control field:
* Output field with no bitmask, controls Output with multiple values
* Output control bit:
* Output with with bitmask, controls Output with a single bit
*
* It is recommended to define callbacks after packet creation with
* setCallback instead of adding it directly here. But you can do it.
* @param {string} group Control group name e.g. "[Channel1]"
* @param {string} name Control name "vu_meter"
* @param {number} offset The field's offset from the start of the packet in bytes:
* - For HID devices which don't use ReportIDs, the data bytes starts at
* position 0
* - For HID devices which use ReportIDs to enumerate the reports, the
* data bytes starts at position 1
* @param {string} pack Is one of the field packing types:
* - b signed byte (Int8)
* - B unsigned byte (Uint8)
* - h signed short (Int16 Little-Endian)
* - H unsigned short (Uint16 Little-Endian)
* - i signed integer (Int32 Little-Endian)
* - I unsigned integer (Uint32 Little-Endian)
* @param {number} [bitmask] A bitwise mask of up to 32 bit. All bits set to'1' in this mask are
* considered.
* @param {fieldChangeCallback} [callback] Callback function for the control
*/
addOutput(group, name, offset, pack, bitmask, callback) {
const control_group = this.getGroup(group, true);
const field_id = `${group}.${name}`;
if (control_group === undefined) {
return;
}
if (!(pack in this.packSizes)) {
console.error(`HIDPacket.addOutput - Unknown Output control pack value ${pack}`);
return;
}
// Adjust offset by 1 because the reportId was previously considered part of the payload
// but isn't anymore and we can't be bothered to adjust every single script manually
offset -= 1;
// Check if we are adding a Output bit to existing bitvector
const fieldByOffset = this.getFieldByOffset(offset, pack);
if (fieldByOffset !== undefined) {
if (bitmask === undefined) {
console.error(`HIDPacket.addOutput - Overwrite non-bitmask control ${group}.${name}`);
return;
}
if (fieldByOffset.type !== "bitvector") {
console.error(`HIDPacket.addOutput - Field is not of type bitvector: ${group}.${name}`);
return;
}
const bitVector = /** @type {HIDBitVector} */ (fieldByOffset.value);
bitVector.addOutputMask(group, name, bitmask);
if (this.length < offset) { this.length = offset; }
return;
}
/** @type {packetField} */
const field = {};
field.id = field_id;
field.group = group;
field.name = name;
field.mapped_group = undefined;
field.mapped_name = undefined;
field.pack = pack;
field.offset = offset;
field.end_offset = offset + this.packSizes[field.pack];
field.bitmask = bitmask;
field.callback = callback;
field.toggle = undefined;
const packet_max_value = Math.pow(2, this.packSizes[field.pack] * 8);
const signed = this.signedPackFormats.includes(field.pack);
if (signed) {
field.min = 0 - (packet_max_value / 2) + 1;
field.max = (packet_max_value / 2) - 1;
} else {
field.min = 0;
field.max = packet_max_value - 1;
}
if (bitmask === undefined || bitmask === packet_max_value) {
field.type = "output";
field.value = undefined;
field.delta = undefined;
field.mindelta = undefined;
} else {
// Create new Output bitvector control field, add bit to it
// rewrite name to use bitvector instead
const field_name = `bitvector_${offset}`;
field.type = "bitvector";
field.id = `${group}.${field_name}`;
field.name = field_name;
const bitvector = new HIDBitVector();
bitvector.size = field.max;
bitvector.addOutputMask(group, name, bitmask);
field.value = bitvector;
field.delta = undefined;
field.mindelta = undefined;
}
// Add Output to HID packet
if (this.length < field.end_offset) { this.length = field.end_offset; }
control_group[field.id] = field;
}
/**
* Register a callback to field or a bit vector bit.
* Does not make sense for Output fields but you can do that.
* @param {string} group Control group name e.g. "[Channel1]"
* @param {string} name Control name e.g. "play"
* @param {fieldChangeCallback} callback Callback function for the control
*/
setCallback(group, name, callback) {
const field = this.getField(group, name);
const field_id = `${group}.${name}`;
if (callback === undefined) {
console.error(`HIDPacket.setCallback - Callback to add was undefined for ${field_id}`);
return;
}
if (field === undefined) {
console.error(`HIDPacket.setCallback - Field for ${field_id} not found`);
return;
}
if (field.type === "bitvector") {
const bitVector = /** @type {HIDBitVector} */ (field.value);
for (const bit_id in bitVector.bits) {
const bit = bitVector.bits[bit_id];
if (bit_id !== field_id) {
continue;
}
bit.callback = callback;
return;
}
console.error(`HIDPacket.setCallback - Bit not found ${field_id}`);
} else {
field.callback = callback;
}
}
/**
* This function can be set in script code to ignore a field you don't want to be processed but
* still wanted to define, to make packet format complete from specifications. If field is
* ignored, it is not reported in 'delta' objects.
* @param {string} group Control group name e.g. "[Channel1]"
* @param {string} name Control name "pregain"
* @param {boolean} ignored 'ignored' flag for field to given value (true or false)
*/
setIgnored(group, name, ignored) {
const field = this.getField(group, name);
if (field === undefined) {
console.error(`HIDPacket.setIgnored - Setting ignored flag for ${group} ${name}`);
return;
}
field.ignored = ignored;
}
/**
* Adjust field's minimum delta value.
* Input value changes smaller than this are not reported in delta
* @param {string} group Control group name e.g. "[Channel1]"
* @param {string} name Control name "pregain"
* @param {number} mindelta Minimum delta value.
*/
setMinDelta(group, name, mindelta) {
const field = this.getField(group, name);
if (field === undefined) {
console.error(`HIDPacket.setMinDelta - Adjusting mindelta for ${group} ${name}`);
return;
}
if (field.type === "bitvector") {
console.error("HIDPacket.setMinDelta - Setting mindelta for bitvector packet does not make sense");
return;
}
field.mindelta = mindelta;
}
/**
* Parse bitvector field values, returning object with the named bits set.
* @param {packetField} field Object that describes a field inside of a packet, which can often
* mapped to a Mixxx control.
* @param {number} value Value must be a valid unsigned byte to parse, with enough bits.
* @returns {Object.<string, bitObject>} List of modified bits (delta),
* referred by a string of group and control name separated by a dot
*/
parseBitVector(field, value) {
/**
* Object of bitObjects, referred by a string of group and control name separated by a dot
* @type {Object.<string, bitObject>}
*/
const bits = {};
if (field.type !== "bitvector") {
console.error("HIDPacket.parseBitVector - Field isn't of type bitvector");
return undefined;
}
const bitVector = /** @type {HIDBitVector} */ (field.value);
HIDController.fastForIn(bitVector.bits, (bit_id) => {
const bit = bitVector.bits[bit_id];
const new_value = (bit.bitmask & value) >> bit.bit_offset;
if (bit.value !== undefined && bit.value !== new_value) {
bits[bit_id] = bit;
}
bit.value = new_value;
}
);
return bits;
}
/**
* Parse input packet fields from data.
* Data is expected to be a Packet() received from HID device.
* BitVectors are returned as bits you can iterate separately.
* @param {number[] | ArrayBuffer | Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array} data Data received as InputReport from the device
* @returns {Object.<string, packetField | bitObject>} List of changed fields with new value.
*/
parse(data) {
/**
* Object of packetField or bitObjects, referred by a string of group and control name separated by a dot
* @type {Object.<string, packetField | bitObject>}
*/
const field_changes = {};
// Fast loop implementation over this.groups object
const groupKeyArr = Object.keys(this.groups);
let groupKeyIdx = groupKeyArr.length;
while (groupKeyIdx--) {
const group = this.groups[groupKeyArr[groupKeyIdx]];
// Fast loop implementation over group object
const fieldKeyArr = Object.keys(group);
let fieldKeyIdx = fieldKeyArr.length;
while (fieldKeyIdx--) {
const field = group[fieldKeyArr[fieldKeyIdx]];
if (field === undefined) {
continue;
}
const value = this.unpack(data, field);
if (value === undefined) {
console.error(`HIDPacket.parse - Parsing packet field value for ${group}.${field}`);
return;
}
if (field.type === "bitvector") {
// Bitvector deltas are checked in parseBitVector
const changedBits = this.parseBitVector(field, value);
HIDController.fastForIn(changedBits, (changedBit) => {