sportident.js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.dependency-cruiser.js +233 -0
  2. package/.editorconfig +12 -0
  3. package/.eslintignore +6 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +35 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +17 -0
  6. package/.github/workflows/npm.yml +17 -0
  7. package/LICENSE +21 -0
  8. package/README.md +31 -0
  9. package/babel.config.js +21 -0
  10. package/build-docs.sh +25 -0
  11. package/docs/index.md +9 -0
  12. package/docs/typedoc/index.md +11 -0
  13. package/eslint.base.js +232 -0
  14. package/install.sh +6 -0
  15. package/jest.config.ts +49 -0
  16. package/nx.json +39 -0
  17. package/package.json +51 -0
  18. package/src/SiCard/BaseSiCard.test.ts +187 -0
  19. package/src/SiCard/BaseSiCard.ts +101 -0
  20. package/src/SiCard/IRaceResultData.ts +16 -0
  21. package/src/SiCard/ISiCard.ts +23 -0
  22. package/src/SiCard/ISiCardExamples.ts +4 -0
  23. package/src/SiCard/index.ts +2 -0
  24. package/src/SiCard/raceResultTools.test.ts +260 -0
  25. package/src/SiCard/raceResultTools.ts +150 -0
  26. package/src/SiCard/types/FCard.test.ts +19 -0
  27. package/src/SiCard/types/FCard.ts +14 -0
  28. package/src/SiCard/types/ModernSiCard.test.ts +186 -0
  29. package/src/SiCard/types/ModernSiCard.ts +241 -0
  30. package/src/SiCard/types/PCard.test.ts +19 -0
  31. package/src/SiCard/types/PCard.ts +14 -0
  32. package/src/SiCard/types/SIAC.test.ts +84 -0
  33. package/src/SiCard/types/SIAC.ts +19 -0
  34. package/src/SiCard/types/SiCard10.test.ts +85 -0
  35. package/src/SiCard/types/SiCard10.ts +17 -0
  36. package/src/SiCard/types/SiCard11.test.ts +84 -0
  37. package/src/SiCard/types/SiCard11.ts +19 -0
  38. package/src/SiCard/types/SiCard5.test.ts +149 -0
  39. package/src/SiCard/types/SiCard5.ts +129 -0
  40. package/src/SiCard/types/SiCard6.test.ts +179 -0
  41. package/src/SiCard/types/SiCard6.ts +222 -0
  42. package/src/SiCard/types/SiCard8.test.ts +137 -0
  43. package/src/SiCard/types/SiCard8.ts +129 -0
  44. package/src/SiCard/types/SiCard9.test.ts +132 -0
  45. package/src/SiCard/types/SiCard9.ts +128 -0
  46. package/src/SiCard/types/TCard.test.ts +19 -0
  47. package/src/SiCard/types/TCard.ts +14 -0
  48. package/src/SiCard/types/index.test.ts +26 -0
  49. package/src/SiCard/types/index.ts +15 -0
  50. package/src/SiCard/types/modernSiCardExamples.ts +364 -0
  51. package/src/SiCard/types/siCard5Examples.ts +73 -0
  52. package/src/SiCard/types/siCard6Examples.ts +262 -0
  53. package/src/SiCard/types/siCard8Examples.ts +152 -0
  54. package/src/SiCard/types/siCard9Examples.ts +143 -0
  55. package/src/SiDevice/INavigatorWebSerial.ts +78 -0
  56. package/src/SiDevice/INavigatorWebUsb.ts +62 -0
  57. package/src/SiDevice/ISiDevice.ts +48 -0
  58. package/src/SiDevice/ISiDeviceDriver.ts +35 -0
  59. package/src/SiDevice/README.md +13 -0
  60. package/src/SiDevice/SiDevice.test.ts +354 -0
  61. package/src/SiDevice/SiDevice.ts +132 -0
  62. package/src/SiDevice/WebSerialSiDeviceDriver.ts +146 -0
  63. package/src/SiDevice/WebUsbSiDeviceDriver.ts +343 -0
  64. package/src/SiDevice/index.ts +3 -0
  65. package/src/SiDevice/testUtils/index.ts +2 -0
  66. package/src/SiDevice/testUtils/testISiDeviceDriver.ts +63 -0
  67. package/src/SiDevice/testUtils/testISiDeviceDriverWithAutodetection.ts +72 -0
  68. package/src/SiStation/BaseSiStation.test.ts +221 -0
  69. package/src/SiStation/BaseSiStation.ts +253 -0
  70. package/src/SiStation/CoupledSiStation.test.ts +41 -0
  71. package/src/SiStation/CoupledSiStation.ts +130 -0
  72. package/src/SiStation/ISiMainStation.ts +29 -0
  73. package/src/SiStation/ISiSendTask.ts +9 -0
  74. package/src/SiStation/ISiStation.ts +88 -0
  75. package/src/SiStation/ISiTargetMultiplexer.ts +51 -0
  76. package/src/SiStation/SiMainStation.test.ts +222 -0
  77. package/src/SiStation/SiMainStation.ts +133 -0
  78. package/src/SiStation/SiSendTask.ts +50 -0
  79. package/src/SiStation/SiTargetMultiplexer.targeting.test.ts +112 -0
  80. package/src/SiStation/SiTargetMultiplexer.test.ts +605 -0
  81. package/src/SiStation/SiTargetMultiplexer.ts +241 -0
  82. package/src/SiStation/index.ts +5 -0
  83. package/src/SiStation/siStationExamples.ts +103 -0
  84. package/src/constants.test.ts +17 -0
  85. package/src/constants.ts +92 -0
  86. package/src/fakes/FakeSiCard/BaseFakeSiCard.test.ts +11 -0
  87. package/src/fakes/FakeSiCard/BaseFakeSiCard.ts +10 -0
  88. package/src/fakes/FakeSiCard/IFakeSiCard.ts +6 -0
  89. package/src/fakes/FakeSiCard/index.ts +2 -0
  90. package/src/fakes/FakeSiCard/types/FakeModernSiCard.test.ts +62 -0
  91. package/src/fakes/FakeSiCard/types/FakeModernSiCard.ts +43 -0
  92. package/src/fakes/FakeSiCard/types/FakeSIAC.ts +17 -0
  93. package/src/fakes/FakeSiCard/types/FakeSiCard10.ts +17 -0
  94. package/src/fakes/FakeSiCard/types/FakeSiCard11.ts +17 -0
  95. package/src/fakes/FakeSiCard/types/FakeSiCard5.test.ts +42 -0
  96. package/src/fakes/FakeSiCard/types/FakeSiCard5.ts +40 -0
  97. package/src/fakes/FakeSiCard/types/FakeSiCard6.test.ts +62 -0
  98. package/src/fakes/FakeSiCard/types/FakeSiCard6.ts +44 -0
  99. package/src/fakes/FakeSiCard/types/FakeSiCard8.ts +16 -0
  100. package/src/fakes/FakeSiCard/types/FakeSiCard9.ts +16 -0
  101. package/src/fakes/FakeSiCard/types/index.ts +7 -0
  102. package/src/fakes/FakeSiDeviceDriver.ts +148 -0
  103. package/src/fakes/FakeSiMainStation.test.ts +141 -0
  104. package/src/fakes/FakeSiMainStation.ts +118 -0
  105. package/src/fakes/IFakeSiMainStation.ts +15 -0
  106. package/src/fakes/index.ts +2 -0
  107. package/src/index.ts +24 -0
  108. package/src/siProtocol.test.ts +509 -0
  109. package/src/siProtocol.ts +417 -0
  110. package/src/storage/SiArray.test.ts +103 -0
  111. package/src/storage/SiArray.ts +56 -0
  112. package/src/storage/SiBool.test.ts +81 -0
  113. package/src/storage/SiBool.ts +47 -0
  114. package/src/storage/SiDataType.test.ts +81 -0
  115. package/src/storage/SiDataType.ts +60 -0
  116. package/src/storage/SiDict.test.ts +115 -0
  117. package/src/storage/SiDict.ts +60 -0
  118. package/src/storage/SiEnum.test.ts +77 -0
  119. package/src/storage/SiEnum.ts +48 -0
  120. package/src/storage/SiFieldValue.test.ts +58 -0
  121. package/src/storage/SiFieldValue.ts +23 -0
  122. package/src/storage/SiInt.test.ts +80 -0
  123. package/src/storage/SiInt.ts +84 -0
  124. package/src/storage/SiModified.test.ts +135 -0
  125. package/src/storage/SiModified.ts +59 -0
  126. package/src/storage/SiStorage.test.ts +51 -0
  127. package/src/storage/SiStorage.ts +44 -0
  128. package/src/storage/index.test.ts +222 -0
  129. package/src/storage/index.ts +12 -0
  130. package/src/storage/interfaces.ts +41 -0
  131. package/src/storage/siStringEncoding.ts +1361 -0
  132. package/src/testUtils.test.ts +266 -0
  133. package/src/testUtils.ts +75 -0
  134. package/src/utils/NumberRange.test.ts +66 -0
  135. package/src/utils/NumberRange.ts +46 -0
  136. package/src/utils/NumberRangeRegistry.test.ts +49 -0
  137. package/src/utils/NumberRangeRegistry.ts +43 -0
  138. package/src/utils/bytes.test.ts +126 -0
  139. package/src/utils/bytes.ts +69 -0
  140. package/src/utils/errors.test.ts +29 -0
  141. package/src/utils/errors.ts +20 -0
  142. package/src/utils/events.test.ts +112 -0
  143. package/src/utils/events.ts +68 -0
  144. package/src/utils/general.test.ts +139 -0
  145. package/src/utils/general.ts +69 -0
  146. package/src/utils/index.ts +8 -0
  147. package/src/utils/mixins.test.ts +40 -0
  148. package/src/utils/mixins.ts +13 -0
  149. package/src/utils/typed.ts +3 -0
  150. package/tsconfig.base.json +22 -0
  151. package/tsconfig.json +15 -0
@@ -0,0 +1,149 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+ import { proto } from '../../constants';
3
+ import type * as siProtocol from '../../siProtocol';
4
+ import * as testUtils from '../../testUtils';
5
+ import { BaseSiCard } from '../BaseSiCard';
6
+ import { cropPunches, getPunchOffset, SiCard5 } from './SiCard5';
7
+ import { getSiCard5Examples } from './siCard5Examples';
8
+ import { FakeSiCard5 } from '../../fakes/FakeSiCard/types/FakeSiCard5';
9
+
10
+ describe('SiCard5', () => {
11
+ test('is registered', () => {
12
+ expect(BaseSiCard.getTypeByCardNumber(999)).not.toEqual(SiCard5);
13
+ expect(BaseSiCard.getTypeByCardNumber(1000)).toEqual(SiCard5);
14
+ expect(BaseSiCard.getTypeByCardNumber(9999)).toEqual(SiCard5);
15
+ expect(BaseSiCard.getTypeByCardNumber(10000)).toEqual(SiCard5);
16
+ expect(BaseSiCard.getTypeByCardNumber(99999)).toEqual(SiCard5);
17
+ expect(BaseSiCard.getTypeByCardNumber(100000)).toEqual(SiCard5);
18
+ expect(BaseSiCard.getTypeByCardNumber(499999)).toEqual(SiCard5);
19
+ expect(BaseSiCard.getTypeByCardNumber(500000)).not.toEqual(SiCard5);
20
+ });
21
+ describe('typeSpecificInstanceFromMessage', () => {
22
+ test('works for valid message', () => {
23
+ const instance = SiCard5.typeSpecificInstanceFromMessage({
24
+ command: proto.cmd.SI5_DET,
25
+ parameters: [0x00, 0x00, 0x00, 0x01, 0x23, 0x45]
26
+ });
27
+ if (instance === undefined) {
28
+ throw new Error('expect instance');
29
+ }
30
+ expect(instance instanceof SiCard5).toBe(true);
31
+ expect(instance.cardNumber).toBe(109029);
32
+ });
33
+ test('returns undefined when message has mode', () => {
34
+ expect(
35
+ SiCard5.typeSpecificInstanceFromMessage({
36
+ mode: proto.NAK
37
+ })
38
+ ).toBe(undefined);
39
+ });
40
+ test('returns undefined when message has wrong command', () => {
41
+ expect(
42
+ SiCard5.typeSpecificInstanceFromMessage({
43
+ command: testUtils.getRandomByteExcept([proto.cmd.SI5_DET]),
44
+ parameters: []
45
+ })
46
+ ).toBe(undefined);
47
+ });
48
+ test('returns undefined when there are too few parameters', () => {
49
+ expect(
50
+ SiCard5.typeSpecificInstanceFromMessage({
51
+ command: proto.cmd.SI5_DET,
52
+ parameters: []
53
+ })
54
+ ).toBe(undefined);
55
+ });
56
+ });
57
+ test('getPunchOffset', () => {
58
+ expect(getPunchOffset(0)).toEqual(0x21);
59
+ expect(getPunchOffset(1)).toEqual(0x24);
60
+ expect(getPunchOffset(2)).toEqual(0x27);
61
+ expect(getPunchOffset(3)).toEqual(0x2a);
62
+ expect(getPunchOffset(4)).toEqual(0x2d);
63
+ expect(getPunchOffset(5)).toEqual(0x31);
64
+ expect(getPunchOffset(9)).toEqual(0x3d);
65
+ expect(getPunchOffset(10)).toEqual(0x41);
66
+ expect(getPunchOffset(14)).toEqual(0x4d);
67
+ expect(getPunchOffset(15)).toEqual(0x51);
68
+ expect(getPunchOffset(19)).toEqual(0x5d);
69
+ expect(getPunchOffset(20)).toEqual(0x61);
70
+ expect(getPunchOffset(24)).toEqual(0x6d);
71
+ expect(getPunchOffset(25)).toEqual(0x71);
72
+ expect(getPunchOffset(29)).toEqual(0x7d);
73
+ expect(getPunchOffset(30)).toEqual(0x20);
74
+ expect(getPunchOffset(31)).toEqual(0x30);
75
+ expect(getPunchOffset(32)).toEqual(0x40);
76
+ expect(getPunchOffset(33)).toEqual(0x50);
77
+ expect(getPunchOffset(34)).toEqual(0x60);
78
+ expect(getPunchOffset(35)).toEqual(0x70);
79
+ });
80
+ test('cropPunches', () => {
81
+ expect(cropPunches([])).toEqual([]);
82
+ expect(cropPunches([{ code: 31, time: {time:1} }])).toEqual([{ code: 31, time: {time:1} }]);
83
+ expect(cropPunches([{ code: 0, time: {time:1} }])).toEqual([]);
84
+ expect(cropPunches([{ code: undefined, time: undefined }])).toEqual([]);
85
+ expect(cropPunches([{ code: 33, time: undefined }])).toEqual([{ code: 33, time: undefined }]);
86
+ expect(
87
+ cropPunches([
88
+ { code: 31, time: {time:1} },
89
+ { code: 32, time: {time:2} },
90
+ { code: 33, time: {time:3} },
91
+ { code: 0, time: {time:4} },
92
+ { code: undefined, time: undefined }
93
+ ])
94
+ ).toEqual([
95
+ { code: 31, time: {time:1} },
96
+ { code: 32, time: {time:2} },
97
+ { code: 33, time: {time:3} }
98
+ ]);
99
+ });
100
+ test('typeSpecificRead fails without mainStation', (done) => {
101
+ const mySiCard5 = new SiCard5(1);
102
+ mySiCard5.typeSpecificRead().then(
103
+ () => done(new Error('expect reject')),
104
+ () => done()
105
+ );
106
+ });
107
+ const examples = getSiCard5Examples();
108
+ Object.keys(examples).forEach((exampleName) => {
109
+ const { storageData, cardData } = examples[exampleName];
110
+ const myFakeSiCard5 = new FakeSiCard5(storageData);
111
+ const mainStationSimulation = {
112
+ sendMessage: (message: siProtocol.SiMessage, numResponses?: number) => {
113
+ const responses: siProtocol.SiMessage[] = myFakeSiCard5.handleRequest(message);
114
+ if (responses.length !== numResponses) {
115
+ throw new Error('Invalid numResponses');
116
+ }
117
+ return Promise.resolve(responses.map((response: siProtocol.SiMessage) => (response.mode === undefined ? [0x00, 0x00, ...response.parameters] : [])));
118
+ }
119
+ };
120
+
121
+ test(`typeSpecificRead works with ${exampleName} example`, (done) => {
122
+ const mySiCard5 = new SiCard5(cardData.cardNumber);
123
+ mySiCard5.mainStation = mainStationSimulation;
124
+ mySiCard5.typeSpecificRead().then(() => {
125
+ expect(mySiCard5.raceResult.cardNumber).toEqual(cardData.cardNumber);
126
+ expect(mySiCard5.raceResult.startTime).toEqual(cardData.startTime);
127
+ expect(mySiCard5.raceResult.finishTime).toEqual(cardData.finishTime);
128
+ expect(mySiCard5.raceResult.checkTime).toEqual(cardData.checkTime);
129
+ expect(mySiCard5.raceResult.punches).toEqual(cardData.punches);
130
+ expect(mySiCard5.punchCount).toEqual(cardData.punchCount);
131
+ done();
132
+ });
133
+ });
134
+
135
+ test(`typeSpecificRead works with wrong card number in ${exampleName} example`, (done) => {
136
+ const mySiCard5 = new SiCard5(cardData.cardNumber + 1);
137
+ mySiCard5.mainStation = mainStationSimulation;
138
+ mySiCard5.typeSpecificRead().then(() => {
139
+ expect(mySiCard5.raceResult.cardNumber).toEqual(cardData.cardNumber);
140
+ expect(mySiCard5.raceResult.startTime).toEqual(cardData.startTime);
141
+ expect(mySiCard5.raceResult.finishTime).toEqual(cardData.finishTime);
142
+ expect(mySiCard5.raceResult.checkTime).toEqual(cardData.checkTime);
143
+ expect(mySiCard5.raceResult.punches).toEqual(cardData.punches);
144
+ expect(mySiCard5.punchCount).toEqual(cardData.punchCount);
145
+ done();
146
+ });
147
+ });
148
+ });
149
+ });
@@ -0,0 +1,129 @@
1
+ import _ from 'lodash';
2
+ import * as storage from '../../storage';
3
+ import * as siProtocol from '../../siProtocol';
4
+ import { proto } from '../../constants';
5
+ import type { IBaseSiCardStorageFields } from '../ISiCard';
6
+ import { BaseSiCard } from '../BaseSiCard';
7
+ import type { IPunch } from '../IRaceResultData';
8
+
9
+ const bytesPerPage = 128;
10
+ const MAX_NUM_PUNCHES = 36;
11
+
12
+ interface PotentialSiCard5Punch {
13
+ code: number | undefined;
14
+ time: siProtocol.SiTimestamp | undefined;
15
+ }
16
+
17
+ export const getPunchOffset = (i: number): number => (i >= 30 ? 0x20 + (i - 30) * 16 : 0x20 + Math.floor(i / 5) + 1 + i * 3);
18
+
19
+ export const cropPunches = (allPunches: (PotentialSiCard5Punch | undefined)[]): IPunch[] => {
20
+ const isPunchEntryValid = (punch: PotentialSiCard5Punch | undefined): punch is IPunch => punch !== undefined && punch.code !== undefined && punch.code !== 0x00;
21
+ const firstInvalidIndex = allPunches.findIndex((punch) => !isPunchEntryValid(punch));
22
+ const punchesUntilInvalid = firstInvalidIndex === -1 ? allPunches : allPunches.slice(0, firstInvalidIndex);
23
+ return punchesUntilInvalid.filter<IPunch>(isPunchEntryValid);
24
+ };
25
+
26
+ export interface ISiCard5StorageFields extends IBaseSiCardStorageFields {
27
+ softwareVersion: number;
28
+ cardHolder: {
29
+ countryCode: number | undefined;
30
+ clubCode: number | undefined;
31
+ };
32
+ }
33
+
34
+ export const siCard5StorageLocations: storage.ISiStorageLocations<ISiCard5StorageFields> = {
35
+ cardNumber: new storage.SiModified(
36
+ new storage.SiArray(3, (i) => new storage.SiInt([[[0x05, 0x04, 0x06][i]]])),
37
+ (extractedValue) => siProtocol.arr2cardNumber(extractedValue)
38
+ // (cardNumber) => siProtocol.cardNumber2arr(cardNumber),
39
+ // (cardNumber) => `${cardNumber}`,
40
+ // (cardNumberString) => parseInt(cardNumberString, 10),
41
+ // (cardNumber) => cardNumber !== undefined && _.isInteger(cardNumber) && cardNumber >= 0,
42
+ ),
43
+ startTime: new siProtocol.SiTime([[0x14], [0x13]]),
44
+ finishTime: new siProtocol.SiTime([[0x16], [0x15]]),
45
+ checkTime: new siProtocol.SiTime([[0x1a], [0x19]]),
46
+ punchCount: new storage.SiModified(new storage.SiInt([[0x17]]), (extractedValue) => extractedValue - 1),
47
+ punches: new storage.SiModified(
48
+ new storage.SiArray(
49
+ MAX_NUM_PUNCHES,
50
+ (i) =>
51
+ new storage.SiDict({
52
+ code: new storage.SiInt([[getPunchOffset(i) + 0]]),
53
+ time: new siProtocol.SiTime(i >= 30 ? undefined : [[getPunchOffset(i) + 2], [getPunchOffset(i) + 1]])
54
+ })
55
+ ),
56
+ (allPunches) => cropPunches(allPunches)
57
+ ),
58
+ cardHolder: new storage.SiDict({
59
+ countryCode: new storage.SiInt([[0x01]]),
60
+ clubCode: new storage.SiInt([[0x03], [0x02]])
61
+ }),
62
+ softwareVersion: new storage.SiInt([[0x1b]])
63
+ };
64
+ export const siCard5StorageDefinition = storage.defineStorage(0x80, siCard5StorageLocations);
65
+
66
+ export class SiCard5 extends BaseSiCard {
67
+ static maxNumPunches = MAX_NUM_PUNCHES;
68
+
69
+ static typeSpecificInstanceFromMessage(message: siProtocol.SiMessage): SiCard5 | undefined {
70
+ if (message.mode !== undefined) {
71
+ return undefined;
72
+ }
73
+ if (message.command !== proto.cmd.SI5_DET) {
74
+ return undefined;
75
+ }
76
+ if (message.parameters.length < 6) {
77
+ return undefined;
78
+ }
79
+ const cardNumber = siProtocol.arr2cardNumber([message.parameters[5], message.parameters[4], message.parameters[3]]);
80
+ /* istanbul ignore next */
81
+ if (cardNumber === undefined) {
82
+ throw new Error('card number cannot be undefined');
83
+ }
84
+ return new this(cardNumber);
85
+ }
86
+
87
+ public storage: storage.ISiStorage<ISiCard5StorageFields>;
88
+
89
+ public punchCount?: number;
90
+ public softwareVersion?: number;
91
+
92
+ constructor(cardNumber: number) {
93
+ super(cardNumber);
94
+ this.storage = siCard5StorageDefinition();
95
+ }
96
+
97
+ typeSpecificRead(): Promise<void> {
98
+ if (!this.mainStation) {
99
+ return Promise.reject(new Error('No main station'));
100
+ }
101
+ return this.mainStation
102
+ .sendMessage(
103
+ {
104
+ command: proto.cmd.GET_SI5,
105
+ parameters: []
106
+ },
107
+ 1
108
+ )
109
+ .then((data: number[][]) => {
110
+ this.storage.splice(bytesPerPage * 0, bytesPerPage, ...data[0].slice(2));
111
+
112
+ const readCardNumber = this.storage.get('cardNumber')!.value;
113
+ if (this.cardNumber !== readCardNumber) {
114
+ console.warn(`SICard5 Number ${readCardNumber} (expected ${this.cardNumber})`);
115
+ }
116
+ this.raceResult = {
117
+ cardNumber: this.storage.get('cardNumber')!.value,
118
+ startTime: this.storage.get('startTime')?.value,
119
+ finishTime: this.storage.get('finishTime')?.value,
120
+ checkTime: this.storage.get('checkTime')?.value,
121
+ punches: this.storage.get('punches')!.value,
122
+ cardHolder: this.storage.get('cardHolder')!.value
123
+ };
124
+ this.punchCount = this.storage.get('punchCount')!.value;
125
+ this.softwareVersion = this.storage.get('softwareVersion')!.value;
126
+ });
127
+ }
128
+ }
129
+ BaseSiCard.registerNumberRange(1000, 500000, SiCard5);
@@ -0,0 +1,179 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+ import _ from 'lodash';
3
+ import { proto } from '../../constants';
4
+ import type * as siProtocol from '../../siProtocol';
5
+ import * as testUtils from '../../testUtils';
6
+ import { BaseSiCard } from '../BaseSiCard';
7
+ import { cropPunches, getPunchOffset, SiCard6 } from './SiCard6';
8
+ import { getSiCard6Examples } from './siCard6Examples';
9
+ import { FakeSiCard6 } from '../../fakes/FakeSiCard/types/FakeSiCard6';
10
+
11
+ describe('SiCard6', () => {
12
+ test('is registered', () => {
13
+ expect(BaseSiCard.getTypeByCardNumber(499999)).not.toEqual(SiCard6);
14
+ expect(BaseSiCard.getTypeByCardNumber(500000)).toEqual(SiCard6);
15
+ expect(BaseSiCard.getTypeByCardNumber(999999)).toEqual(SiCard6);
16
+ expect(BaseSiCard.getTypeByCardNumber(1000000)).not.toEqual(SiCard6);
17
+ expect(BaseSiCard.getTypeByCardNumber(2002999)).not.toEqual(SiCard6);
18
+ expect(BaseSiCard.getTypeByCardNumber(2003000)).toEqual(SiCard6);
19
+ expect(BaseSiCard.getTypeByCardNumber(2003999)).toEqual(SiCard6);
20
+ expect(BaseSiCard.getTypeByCardNumber(2004000)).not.toEqual(SiCard6);
21
+ });
22
+ describe('typeSpecificInstanceFromMessage', () => {
23
+ test('works for valid message', () => {
24
+ const instance = SiCard6.typeSpecificInstanceFromMessage({
25
+ command: proto.cmd.SI6_DET,
26
+ parameters: [0x00, 0x00, 0x00, 0x08, 0x88, 0x88]
27
+ });
28
+ if (instance === undefined) {
29
+ throw new Error('expect instance');
30
+ }
31
+ expect(instance instanceof SiCard6).toBe(true);
32
+ expect(instance.cardNumber).toBe(559240);
33
+ });
34
+ test('returns undefined when message has mode', () => {
35
+ expect(
36
+ SiCard6.typeSpecificInstanceFromMessage({
37
+ mode: proto.NAK
38
+ })
39
+ ).toBe(undefined);
40
+ });
41
+ test('returns undefined when message has wrong command', () => {
42
+ expect(
43
+ SiCard6.typeSpecificInstanceFromMessage({
44
+ command: testUtils.getRandomByteExcept([proto.cmd.SI6_DET]),
45
+ parameters: []
46
+ })
47
+ ).toBe(undefined);
48
+ });
49
+ test('returns undefined when there are too few parameters', () => {
50
+ expect(
51
+ SiCard6.typeSpecificInstanceFromMessage({
52
+ command: proto.cmd.SI6_DET,
53
+ parameters: []
54
+ })
55
+ ).toBe(undefined);
56
+ });
57
+ });
58
+ test('getPunchOffset', () => {
59
+ expect(getPunchOffset(0)).toEqual(0x300);
60
+ expect(getPunchOffset(1)).toEqual(0x304);
61
+ expect(getPunchOffset(63)).toEqual(0x3fc);
62
+ });
63
+ test('cropPunches', () => {
64
+ expect(cropPunches([])).toEqual([]);
65
+ expect(cropPunches([{ code: 31, time: {time:1} }])).toEqual([{ code: 31, time: {time:1} }]);
66
+ expect(cropPunches([{ code: 32, time: null }])).toEqual([]);
67
+ expect(cropPunches([{ code: undefined, time: undefined }])).toEqual([]);
68
+ expect(cropPunches([{ code: 33, time: undefined }])).toEqual([]);
69
+ expect(
70
+ cropPunches([
71
+ { code: 31, time: {time:1} },
72
+ { code: 32, time: {time:2} },
73
+ { code: 33, time: {time:3} },
74
+ { code: 0xee, time: null },
75
+ { code: undefined, time: undefined }
76
+ ])
77
+ ).toEqual([
78
+ { code: 31, time: {time:1} },
79
+ { code: 32, time: {time:2} },
80
+ { code: 33, time: {time:3} }
81
+ ]);
82
+ });
83
+ test('typeSpecificRead fails without mainStation', (done) => {
84
+ const mySiCard6 = new SiCard6(1);
85
+ mySiCard6.typeSpecificRead().then(
86
+ () => done(new Error('expect reject')),
87
+ () => done()
88
+ );
89
+ });
90
+ const examples = getSiCard6Examples();
91
+ Object.keys(examples).forEach((exampleName) => {
92
+ const { storageData, cardData } = examples[exampleName];
93
+ const myFakeSiCard6 = new FakeSiCard6(storageData);
94
+ const mainStationSimulation = {
95
+ sendMessage: (message: siProtocol.SiMessage, numResponses?: number) => {
96
+ const responses: siProtocol.SiMessage[] = myFakeSiCard6.handleRequest(message);
97
+ if (responses.length !== numResponses) {
98
+ throw new Error('Invalid numResponses');
99
+ }
100
+ return Promise.resolve(responses.map((response: siProtocol.SiMessage) => (response.mode === undefined ? [0x00, 0x00, ...response.parameters] : [])));
101
+ }
102
+ };
103
+
104
+ test(`typeSpecificRead works with ${exampleName} example`, (done) => {
105
+ const mySiCard6 = new SiCard6(cardData.cardNumber);
106
+ mySiCard6.mainStation = mainStationSimulation;
107
+ mySiCard6.typeSpecificRead().then(() => {
108
+ expect(mySiCard6.raceResult.cardNumber).toEqual(cardData.cardNumber);
109
+ expect(mySiCard6.raceResult.startTime).toEqual(cardData.startTime);
110
+ expect(mySiCard6.raceResult.finishTime).toEqual(cardData.finishTime);
111
+ expect(mySiCard6.raceResult.clearTime).toEqual(cardData.clearTime);
112
+ expect(mySiCard6.raceResult.checkTime).toEqual(cardData.checkTime);
113
+ expect(mySiCard6.raceResult.punches).toEqual(cardData.punches);
114
+ expect(mySiCard6.raceResult.cardHolder).toEqual(cardData.cardHolder);
115
+ expect(mySiCard6.punchCount).toEqual(cardData.punchCount);
116
+ expect(mySiCard6.punchCountPlus1).toEqual(cardData.punchCountPlus1);
117
+ expect(mySiCard6.lastPunchedCode).toEqual(cardData.lastPunchedCode);
118
+ done();
119
+ });
120
+ });
121
+
122
+ test(`typeSpecificRead works with wrong card number in ${exampleName} example`, (done) => {
123
+ const mySiCard6 = new SiCard6(cardData.cardNumber + 1);
124
+ mySiCard6.mainStation = mainStationSimulation;
125
+ mySiCard6.typeSpecificRead().then(() => {
126
+ expect(mySiCard6.raceResult.cardNumber).toEqual(cardData.cardNumber);
127
+ expect(mySiCard6.raceResult.startTime).toEqual(cardData.startTime);
128
+ expect(mySiCard6.raceResult.finishTime).toEqual(cardData.finishTime);
129
+ expect(mySiCard6.raceResult.clearTime).toEqual(cardData.clearTime);
130
+ expect(mySiCard6.raceResult.checkTime).toEqual(cardData.checkTime);
131
+ expect(mySiCard6.raceResult.punches).toEqual(cardData.punches);
132
+ expect(mySiCard6.raceResult.cardHolder).toEqual(cardData.cardHolder);
133
+ expect(mySiCard6.punchCount).toEqual(cardData.punchCount);
134
+ expect(mySiCard6.punchCountPlus1).toEqual(cardData.punchCountPlus1);
135
+ expect(mySiCard6.lastPunchedCode).toEqual(cardData.lastPunchedCode);
136
+ done();
137
+ });
138
+ });
139
+ });
140
+ test('typeSpecificRead if typeSpecificReadCardHolder fails', (done) => {
141
+ const testError = new Error('test');
142
+ let typeSpecificReadCardHolderCalled = false;
143
+ class SiCard6WithoutCardHolder extends SiCard6 {
144
+ typeSpecificGetPage() {
145
+ return Promise.resolve(_.range(128).map(() => 0x00));
146
+ }
147
+
148
+ typeSpecificReadCardHolder() {
149
+ typeSpecificReadCardHolderCalled = true;
150
+ return Promise.reject(testError);
151
+ }
152
+ }
153
+ const mySiCard6 = new SiCard6WithoutCardHolder(654321);
154
+ mySiCard6.typeSpecificRead().catch((err) => {
155
+ expect(typeSpecificReadCardHolderCalled).toBe(true);
156
+ expect(err).toBe(testError);
157
+ done();
158
+ });
159
+ });
160
+ test('typeSpecificRead if typeSpecificReadPunches fails', (done) => {
161
+ const testError = new Error('test');
162
+ let attemptedToGetPage6 = false;
163
+ class SiCard6WithoutCardHolder extends SiCard6 {
164
+ typeSpecificGetPage(pageNumber: number) {
165
+ if (pageNumber === 6) {
166
+ attemptedToGetPage6 = true;
167
+ return Promise.reject(testError);
168
+ }
169
+ return Promise.resolve(_.range(128).map(() => 0x01));
170
+ }
171
+ }
172
+ const myModernSiCard = new SiCard6WithoutCardHolder(7123456);
173
+ myModernSiCard.typeSpecificRead().catch((err) => {
174
+ expect(attemptedToGetPage6).toBe(true);
175
+ expect(err).toBe(testError);
176
+ done();
177
+ });
178
+ });
179
+ });
@@ -0,0 +1,222 @@
1
+ import _ from 'lodash';
2
+ import * as storage from '../../storage';
3
+ import * as siProtocol from '../../siProtocol';
4
+ import { proto } from '../../constants';
5
+ import type { IBaseSiCardStorageFields } from '../ISiCard';
6
+ import { BaseSiCard } from '../BaseSiCard';
7
+ import type { IPunch } from '../IRaceResultData';
8
+
9
+ class ReadFinishedException {}
10
+ const punchesPerPage = 32;
11
+ const bytesPerPage = 128;
12
+ const MAX_NUM_PUNCHES = 64;
13
+
14
+ interface PotentialSiCard6Punch {
15
+ code: number | undefined;
16
+ time: siProtocol.SiTimestamp | undefined;
17
+ }
18
+
19
+ export const getPunchOffset = (i: number): number => bytesPerPage * 6 + i * 4;
20
+
21
+ export const cropPunches = (allPunches: (PotentialSiCard6Punch | undefined)[]): IPunch[] => {
22
+ const isPunchEntryValid = (punch: PotentialSiCard6Punch | undefined): punch is IPunch => punch !== undefined && punch.code !== undefined && punch.time !== undefined && punch.time !== null;
23
+ const firstInvalidIndex = allPunches.findIndex((punch) => !isPunchEntryValid(punch));
24
+ const punchesUntilInvalid = firstInvalidIndex === -1 ? allPunches : allPunches.slice(0, firstInvalidIndex);
25
+ return punchesUntilInvalid.filter<IPunch>(isPunchEntryValid);
26
+ };
27
+
28
+ const getCroppedString = (maybeCharCodes: (number | undefined)[]) => {
29
+ if (maybeCharCodes.some((maybeCharCode: number | undefined) => maybeCharCode === undefined)) {
30
+ return undefined;
31
+ }
32
+ const charCodes = maybeCharCodes as number[];
33
+ const isCharacterInvalid = (charCode: number) => charCode === 0x20;
34
+ const firstInvalidIndex = charCodes.findIndex(isCharacterInvalid);
35
+ const croppedCharCodes = firstInvalidIndex === -1 ? charCodes : charCodes.slice(0, firstInvalidIndex);
36
+ return croppedCharCodes.map((charCode: number) => String.fromCharCode(charCode)).join('');
37
+ };
38
+
39
+ export interface ISiCard6StorageFields extends IBaseSiCardStorageFields {
40
+ clearTime: siProtocol.SiTimestamp;
41
+ lastPunchedCode: number;
42
+ punchCountPlus1: number;
43
+ cardHolder: {
44
+ lastName: string | undefined;
45
+ firstName: string | undefined;
46
+ country: string | undefined;
47
+ club: string | undefined;
48
+ userId: string | undefined;
49
+ phone: string | undefined;
50
+ email: string | undefined;
51
+ street: string | undefined;
52
+ city: string | undefined;
53
+ zip: string | undefined;
54
+ gender: string | undefined;
55
+ birthday: string | undefined;
56
+ isComplete: boolean | undefined;
57
+ };
58
+ }
59
+
60
+ export const siCard6StorageLocations: storage.ISiStorageLocations<ISiCard6StorageFields> = {
61
+ cardNumber: new storage.SiModified(
62
+ new storage.SiArray(3, (i) => new storage.SiInt([[0x0b + (2 - i)]])),
63
+ (extractedValue) => siProtocol.arr2cardNumber(extractedValue)
64
+ // (cardNumber) => siProtocol.cardNumber2arr(cardNumber),
65
+ // (cardNumber) => `${cardNumber}`,
66
+ // (cardNumberString) => parseInt(cardNumberString, 10),
67
+ // (cardNumber) => cardNumber !== undefined && _.isInteger(cardNumber) && cardNumber >= 0,
68
+ ),
69
+ startTime: new siProtocol.SiTime([[0x1b], [0x1a], [0x18]]),
70
+ finishTime: new siProtocol.SiTime([[0x17], [0x16], [0x14]]),
71
+ checkTime: new siProtocol.SiTime([[0x1f], [0x1e], [0x1c]]),
72
+ clearTime: new siProtocol.SiTime([[0x23], [0x22], [0x20]]),
73
+ lastPunchedCode: new storage.SiInt([[0x11], [0x10]]),
74
+ punchCount: new storage.SiInt([[0x12]]),
75
+ punchCountPlus1: new storage.SiInt([[0x13]]),
76
+ punches: new storage.SiModified(
77
+ new storage.SiArray(
78
+ MAX_NUM_PUNCHES,
79
+ (i) =>
80
+ new storage.SiDict({
81
+ code: new storage.SiInt([[getPunchOffset(i) + 1]]),
82
+ time: new siProtocol.SiTime([[getPunchOffset(i) + 3], [getPunchOffset(i) + 2], [getPunchOffset(i) + 0]])
83
+ })
84
+ ),
85
+ (allPunches) => cropPunches(allPunches)
86
+ ),
87
+ cardHolder: new storage.SiDict({
88
+ lastName: new storage.SiModified(new storage.SiArray(20, (i) => new storage.SiInt([[0x30 + i]])), (charCodes) => getCroppedString(charCodes)),
89
+ firstName: new storage.SiModified(new storage.SiArray(20, (i) => new storage.SiInt([[0x44 + i]])), (charCodes) => getCroppedString(charCodes)),
90
+ country: new storage.SiModified(new storage.SiArray(4, (i) => new storage.SiInt([[0x58 + i]])), (charCodes) => getCroppedString(charCodes)),
91
+ club: new storage.SiModified(new storage.SiArray(36, (i) => new storage.SiInt([[0x5c + i]])), (charCodes) => getCroppedString(charCodes)),
92
+ userId: new storage.SiModified(new storage.SiArray(16, (i) => new storage.SiInt([[0x80 + i]])), (charCodes) => getCroppedString(charCodes)),
93
+ phone: new storage.SiModified(new storage.SiArray(16, (i) => new storage.SiInt([[0x90 + i]])), (charCodes) => getCroppedString(charCodes)),
94
+ email: new storage.SiModified(new storage.SiArray(36, (i) => new storage.SiInt([[0xa0 + i]])), (charCodes) => getCroppedString(charCodes)),
95
+ street: new storage.SiModified(new storage.SiArray(20, (i) => new storage.SiInt([[0xc4 + i]])), (charCodes) => getCroppedString(charCodes)),
96
+ city: new storage.SiModified(new storage.SiArray(16, (i) => new storage.SiInt([[0xd8 + i]])), (charCodes) => getCroppedString(charCodes)),
97
+ zip: new storage.SiModified(new storage.SiArray(8, (i) => new storage.SiInt([[0xe8 + i]])), (charCodes) => getCroppedString(charCodes)),
98
+ gender: new storage.SiModified(new storage.SiArray(4, (i) => new storage.SiInt([[0xf0 + i]])), (charCodes) => getCroppedString(charCodes)),
99
+ birthday: new storage.SiModified(new storage.SiArray(8, (i) => new storage.SiInt([[0xf4 + i]])), (charCodes) => getCroppedString(charCodes)),
100
+ isComplete: new storage.SiModified(new storage.SiArray(0xd0, (i) => new storage.SiInt([[0x30 + i]])), (charCodes) => charCodes.every((charCode) => charCode !== undefined))
101
+ })
102
+ };
103
+ export const siCard6StorageDefinition = storage.defineStorage(0x400, siCard6StorageLocations);
104
+
105
+ export class SiCard6 extends BaseSiCard {
106
+ static maxNumPunches = MAX_NUM_PUNCHES;
107
+
108
+ static typeSpecificInstanceFromMessage(message: siProtocol.SiMessage): SiCard6 | undefined {
109
+ if (message.mode !== undefined) {
110
+ return undefined;
111
+ }
112
+ if (message.command !== proto.cmd.SI6_DET) {
113
+ return undefined;
114
+ }
115
+ if (message.parameters.length < 6) {
116
+ return undefined;
117
+ }
118
+ const cardNumber = siProtocol.arr2cardNumber([message.parameters[5], message.parameters[4], message.parameters[3]]);
119
+ /* istanbul ignore next */
120
+ if (cardNumber === undefined) {
121
+ throw new Error('card number cannot be undefined');
122
+ }
123
+ return new this(cardNumber);
124
+ }
125
+
126
+ public storage: storage.ISiStorage<ISiCard6StorageFields>;
127
+
128
+ public punchCount?: number;
129
+ public punchCountPlus1?: number;
130
+ public lastPunchedCode?: number;
131
+
132
+ constructor(cardNumber: number) {
133
+ super(cardNumber);
134
+ this.storage = siCard6StorageDefinition();
135
+ }
136
+
137
+ typeSpecificGetPage(pageNumber: number): Promise<number[]> {
138
+ if (!this.mainStation) {
139
+ return Promise.reject(new Error('No main station'));
140
+ }
141
+ return this.mainStation
142
+ .sendMessage(
143
+ {
144
+ command: proto.cmd.GET_SI6,
145
+ parameters: [pageNumber]
146
+ },
147
+ 1
148
+ )
149
+ .then((data: number[][]) => {
150
+ console.assert(data[0][2] === pageNumber, `Page number ${data[0][2]} retrieved (expected ${pageNumber})`);
151
+ return data[0].slice(3);
152
+ });
153
+ }
154
+
155
+ typeSpecificRead(): Promise<void> {
156
+ return new Promise((resolve, reject) => {
157
+ this.typeSpecificReadBasic()
158
+ .then(() => this.typeSpecificReadCardHolder())
159
+ .then(() => this.typeSpecificReadPunches())
160
+ .then(() => {
161
+ this.raceResult = {
162
+ cardNumber: this.storage.get('cardNumber')!.value,
163
+ startTime: this.storage.get('startTime')?.value,
164
+ finishTime: this.storage.get('finishTime')?.value,
165
+ clearTime: this.storage.get('clearTime')?.value,
166
+ checkTime: this.storage.get('checkTime')?.value,
167
+ punches: this.storage.get('punches')!.value,
168
+ cardHolder: this.storage.get('cardHolder')!.value
169
+ };
170
+ this.punchCount = this.storage.get('punchCount')!.value;
171
+ this.punchCountPlus1 = this.storage.get('punchCountPlus1')!.value;
172
+ this.lastPunchedCode = this.storage.get('lastPunchedCode')!.value;
173
+ resolve();
174
+ })
175
+ .catch((exc: Error) => reject(exc));
176
+ });
177
+ }
178
+
179
+ typeSpecificReadBasic(): Promise<void> {
180
+ return this.typeSpecificGetPage(0).then((page0: number[]) => {
181
+ this.storage.splice(bytesPerPage * 0, bytesPerPage, ...page0);
182
+
183
+ const readCardNumber = this.storage.get('cardNumber')!.value;
184
+ if (this.cardNumber !== readCardNumber) {
185
+ console.warn(`SICard6 Number ${readCardNumber} (expected ${this.cardNumber})`);
186
+ }
187
+ });
188
+ }
189
+
190
+ typeSpecificReadCardHolder(): Promise<void> {
191
+ // TODO: test this with real device
192
+ return this.typeSpecificGetPage(1).then((page1: number[]) => {
193
+ this.storage.splice(bytesPerPage * 1, bytesPerPage, ...page1);
194
+ });
195
+ }
196
+
197
+ typeSpecificReadPunches(): Promise<void> {
198
+ if (this.storage.get('punchCount')!.value <= punchesPerPage * 0) {
199
+ return Promise.resolve();
200
+ }
201
+ return this.typeSpecificGetPage(6)
202
+ .then((page6: number[]) => {
203
+ this.storage.splice(bytesPerPage * 6, bytesPerPage, ...page6);
204
+ if (this.storage.get('punchCount')!.value <= punchesPerPage * 1) {
205
+ throw new ReadFinishedException();
206
+ }
207
+ return this.typeSpecificGetPage(7);
208
+ })
209
+ .then((page7: number[]) => {
210
+ this.storage.splice(bytesPerPage * 7, bytesPerPage, ...page7);
211
+ throw new ReadFinishedException();
212
+ })
213
+ .catch((exc: Error) => {
214
+ if (exc instanceof ReadFinishedException) {
215
+ return;
216
+ }
217
+ throw exc;
218
+ });
219
+ }
220
+ }
221
+ BaseSiCard.registerNumberRange(500000, 1000000, SiCard6);
222
+ BaseSiCard.registerNumberRange(2003000, 2004000, SiCard6);