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,221 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+ import _ from 'lodash';
3
+ import type * as siProtocol from '../siProtocol';
4
+ import * as testUtils from '../testUtils';
5
+ import { proto } from '../constants';
6
+ import { SiDataType } from '../storage';
7
+ import { type ISiTargetMultiplexer, SiTargetMultiplexerTarget } from './ISiTargetMultiplexer';
8
+ import { BaseSiStation } from './BaseSiStation';
9
+ import { getSiStationExamples } from './siStationExamples';
10
+
11
+ testUtils.useFakeTimers();
12
+
13
+ describe('SiStation', () => {
14
+ class MySiStation extends BaseSiStation<SiTargetMultiplexerTarget.Direct> {}
15
+
16
+ test('SiStation info', async () => {
17
+ const mySiStation = new MySiStation({} as unknown as ISiTargetMultiplexer, SiTargetMultiplexerTarget.Direct);
18
+ interface MessageRecord {
19
+ target: SiTargetMultiplexerTarget;
20
+ command: number | undefined;
21
+ parameters: number[] | undefined;
22
+ numResponses: number | undefined;
23
+ }
24
+ const messagesSent: MessageRecord[] = [];
25
+ mySiStation.siTargetMultiplexer = {
26
+ sendMessage: (target, message, numResponses) => {
27
+ const command = message.mode === undefined ? message.command : undefined;
28
+ const parameters = message.mode === undefined ? message.parameters : undefined;
29
+ messagesSent.push({
30
+ target: target,
31
+ command: command,
32
+ parameters: parameters,
33
+ numResponses: numResponses
34
+ });
35
+ if (command === proto.cmd.GET_SYS_VAL) {
36
+ return Promise.resolve([[0x00, 0x00, 0x00, ..._.range(128)]]);
37
+ }
38
+ if (command === proto.cmd.SET_SYS_VAL) {
39
+ return Promise.resolve([[0x00, 0x00, 0x72]]);
40
+ }
41
+ throw new Error();
42
+ }
43
+ } as ISiTargetMultiplexer;
44
+ const timeState = { infoHasBeenRead: false, changesHaveBeenWritten: false };
45
+ mySiStation.readInfo().then(() => {
46
+ timeState.infoHasBeenRead = true;
47
+ });
48
+ expect(mySiStation.storage.data.toJS()).toEqual(_.range(128).map(() => undefined));
49
+ expect(mySiStation.getField('code') instanceof SiDataType).toBe(true);
50
+ expect(mySiStation.getInfo('code')).toBe(undefined);
51
+
52
+ await testUtils.advanceTimersByTime(0);
53
+ expect(messagesSent).toEqual([
54
+ {
55
+ target: SiTargetMultiplexerTarget.Direct,
56
+ command: proto.cmd.GET_SYS_VAL,
57
+ numResponses: 1,
58
+ parameters: [0, 128]
59
+ }
60
+ ]);
61
+ expect(timeState).toEqual({ infoHasBeenRead: true, changesHaveBeenWritten: false });
62
+ expect(mySiStation.storage.data.toJS()).toEqual(_.range(128));
63
+ expect(mySiStation.getInfo('code')).not.toBe(undefined);
64
+ expect(mySiStation.setInfo('code', 0)).toBe(undefined);
65
+ expect(mySiStation.getInfo('code')!.value).toBe(0);
66
+ mySiStation.writeChanges().then(() => {
67
+ timeState.changesHaveBeenWritten = true;
68
+ });
69
+
70
+ await testUtils.nTimesAsync(2, () => testUtils.advanceTimersByTime(0));
71
+ expect(messagesSent).toEqual([
72
+ {
73
+ target: SiTargetMultiplexerTarget.Direct,
74
+ command: proto.cmd.GET_SYS_VAL,
75
+ numResponses: 1,
76
+ parameters: [0, 128]
77
+ },
78
+ {
79
+ target: SiTargetMultiplexerTarget.Direct,
80
+ command: proto.cmd.GET_SYS_VAL,
81
+ numResponses: 1,
82
+ parameters: [0, 128]
83
+ },
84
+ {
85
+ target: SiTargetMultiplexerTarget.Direct,
86
+ command: proto.cmd.SET_SYS_VAL,
87
+ numResponses: 1,
88
+ parameters: [0x72, 0x00, 0x33]
89
+ }
90
+ ]);
91
+ expect(timeState).toEqual({ infoHasBeenRead: true, changesHaveBeenWritten: true });
92
+ mySiStation.atomically(() => {
93
+ mySiStation.setInfo('code', 10);
94
+ });
95
+
96
+ await testUtils.nTimesAsync(2, () => testUtils.advanceTimersByTime(0));
97
+ expect(messagesSent).toEqual([
98
+ {
99
+ target: SiTargetMultiplexerTarget.Direct,
100
+ command: proto.cmd.GET_SYS_VAL,
101
+ numResponses: 1,
102
+ parameters: [0, 128]
103
+ },
104
+ {
105
+ target: SiTargetMultiplexerTarget.Direct,
106
+ command: proto.cmd.GET_SYS_VAL,
107
+ numResponses: 1,
108
+ parameters: [0, 128]
109
+ },
110
+ {
111
+ target: SiTargetMultiplexerTarget.Direct,
112
+ command: proto.cmd.SET_SYS_VAL,
113
+ numResponses: 1,
114
+ parameters: [0x72, 0x00, 0x33]
115
+ },
116
+ {
117
+ target: SiTargetMultiplexerTarget.Direct,
118
+ command: proto.cmd.GET_SYS_VAL,
119
+ numResponses: 1,
120
+ parameters: [0, 128]
121
+ },
122
+ {
123
+ target: SiTargetMultiplexerTarget.Direct,
124
+ command: proto.cmd.SET_SYS_VAL,
125
+ numResponses: 1,
126
+ parameters: [0x72, 0x0a, 0x33]
127
+ }
128
+ ]);
129
+ expect(mySiStation.getInfo('code')!.value).toBe(10);
130
+ });
131
+ const examples = getSiStationExamples();
132
+ Object.keys(examples).forEach((exampleName) => {
133
+ const { storageData, stationData } = examples[exampleName];
134
+ test(`works with ${exampleName} example`, (done) => {
135
+ const mySiStation = new MySiStation(
136
+ {
137
+ sendMessage: (_target: SiTargetMultiplexerTarget, message: siProtocol.SiMessage, numResponses: number) => {
138
+ if (message.mode !== undefined) {
139
+ return Promise.resolve([]);
140
+ }
141
+ const { command, parameters } = message;
142
+ if (command === proto.cmd.GET_SYS_VAL) {
143
+ const getRange = (offset: number, length: number) => [...[0x00, 0x00, offset], ...storageData.slice(offset, offset + length)];
144
+ if (numResponses !== 1) {
145
+ throw new Error(`Invalid numResponses ${numResponses} (expected 1)`);
146
+ }
147
+ return Promise.resolve([getRange(parameters![0]!, parameters![1]!)]);
148
+ }
149
+ return Promise.resolve([]);
150
+ }
151
+ } as unknown as ISiTargetMultiplexer,
152
+ SiTargetMultiplexerTarget.Direct
153
+ );
154
+ mySiStation.readInfo().then(() => {
155
+ Object.keys(stationData).forEach((stationDataKey) => {
156
+ // @ts-ignore
157
+ expect(mySiStation.getInfo(stationDataKey)!.value).toEqual(stationData[stationDataKey]);
158
+ });
159
+ done();
160
+ });
161
+ });
162
+ });
163
+
164
+ test('get/set time', async () => {
165
+ const mySiStation = new MySiStation(
166
+ {
167
+ sendMessage: () => Promise.resolve([[0x00, 0x00, 0x01, 0x0c, 0x1f, 0x00, 0xa8, 0xbf, 0x40]])
168
+ } as unknown as ISiTargetMultiplexer,
169
+ SiTargetMultiplexerTarget.Direct
170
+ );
171
+ const timeState: { [key: string]: Date | undefined } = {
172
+ retrievedTime: undefined,
173
+ setTime: undefined
174
+ };
175
+ mySiStation.getTime().then((time) => {
176
+ timeState.retrievedTime = time;
177
+ });
178
+ await testUtils.nTimesAsync(1, () => testUtils.advanceTimersByTime(0));
179
+ expect(timeState.retrievedTime instanceof Date).toBe(true);
180
+
181
+ mySiStation.setTime(new Date()).then((time) => {
182
+ timeState.setTime = time;
183
+ });
184
+ await testUtils.nTimesAsync(1, () => testUtils.advanceTimersByTime(0));
185
+ expect(timeState.setTime instanceof Date).toBe(true);
186
+ });
187
+ test('signal', async () => {
188
+ const mySiStation = new MySiStation(
189
+ {
190
+ sendMessage: () => Promise.resolve([[0x00, 0x00, 0x02]])
191
+ } as unknown as ISiTargetMultiplexer,
192
+ SiTargetMultiplexerTarget.Direct
193
+ );
194
+ const timeState = { signalTwiceSucceeded: false, signalOnceFailed: false };
195
+ mySiStation.signal(2).then(() => {
196
+ timeState.signalTwiceSucceeded = true;
197
+ });
198
+ await testUtils.nTimesAsync(1, () => testUtils.advanceTimersByTime(0));
199
+ expect(timeState).toEqual({ signalTwiceSucceeded: true, signalOnceFailed: false });
200
+
201
+ mySiStation.signal(1).catch(() => {
202
+ timeState.signalOnceFailed = true;
203
+ });
204
+ await testUtils.nTimesAsync(1, () => testUtils.advanceTimersByTime(0));
205
+ expect(timeState).toEqual({ signalTwiceSucceeded: true, signalOnceFailed: true });
206
+ });
207
+ test('powerOff', async () => {
208
+ const mySiStation = new MySiStation(
209
+ {
210
+ sendMessage: () => Promise.resolve()
211
+ } as unknown as ISiTargetMultiplexer,
212
+ SiTargetMultiplexerTarget.Direct
213
+ );
214
+ const timeState = { powerOffSucceeded: false };
215
+ mySiStation.powerOff().then(() => {
216
+ timeState.powerOffSucceeded = true;
217
+ });
218
+ await testUtils.nTimesAsync(1, () => testUtils.advanceTimersByTime(0));
219
+ expect(timeState).toEqual({ powerOffSucceeded: true });
220
+ });
221
+ });
@@ -0,0 +1,253 @@
1
+ import { proto } from '../constants';
2
+ import * as storage from '../storage';
3
+ import * as siProtocol from '../siProtocol';
4
+ import { type ISiStation, SiStationMode, SiStationModel } from './ISiStation';
5
+ import { type ISiTargetMultiplexer, SiTargetMultiplexerTarget } from './ISiTargetMultiplexer';
6
+
7
+ export interface ISiStationStorageFields {
8
+ code: number;
9
+ mode: keyof typeof SiStationMode;
10
+ beeps: boolean;
11
+ flashes: boolean;
12
+ autoSend: boolean;
13
+ extendedProtocol: boolean;
14
+ serialNumber: number;
15
+ firmwareVersion: number;
16
+ buildDate: Date;
17
+ deviceModel: keyof typeof SiStationModel;
18
+ memorySize: number;
19
+ batteryDate: Date;
20
+ batteryCapacity: number;
21
+ batteryState: number;
22
+ backupPointer: number;
23
+ siCard6Mode: number;
24
+ memoryOverflow: number;
25
+ lastWriteDate: Date;
26
+ autoOffTimeout: number;
27
+ refreshRate: number;
28
+ powerMode: number;
29
+ interval: number;
30
+ wtf: number;
31
+ program: number;
32
+ handshake: boolean;
33
+ sprint4ms: boolean;
34
+ passwordOnly: boolean;
35
+ stopOnFullBackup: boolean;
36
+ autoReadout: boolean;
37
+ sleepDay: number;
38
+ sleepSeconds: number;
39
+ workingMinutes: number;
40
+ }
41
+ export const siStationStorageLocations: storage.ISiStorageLocations<ISiStationStorageFields> = {
42
+ code: new storage.SiInt([[0x72], [0x73, 6, 8]]),
43
+ mode: new storage.SiEnum([[0x71]], SiStationMode),
44
+ beeps: new storage.SiBool(0x73, 2),
45
+ flashes: new storage.SiBool(0x73, 0),
46
+ autoSend: new storage.SiBool(0x74, 1),
47
+ extendedProtocol: new storage.SiBool(0x74, 0),
48
+ serialNumber: new storage.SiInt([[0x03], [0x02], [0x01], [0x00]]),
49
+ firmwareVersion: new storage.SiInt([[0x07], [0x06], [0x05]]),
50
+ buildDate: new siProtocol.SiDate(3, (i) => 0x08 + i),
51
+ deviceModel: new storage.SiEnum([[0x0c], [0x0b]], SiStationModel),
52
+ memorySize: new storage.SiInt([[0x0d]]),
53
+ batteryDate: new siProtocol.SiDate(3, (i) => 0x15 + i),
54
+ batteryCapacity: new storage.SiInt([[0x1a], [0x19]]),
55
+ batteryState: new storage.SiInt([[0x37], [0x36], [0x35], [0x34]]),
56
+ // 2000mAh: 000000=0%, 6E0000=100%, 1000mAh:000000=0%, 370000=100%
57
+ backupPointer: new storage.SiInt([[0x22], [0x21], [0x1d], [0x1c]]),
58
+ siCard6Mode: new storage.SiInt([[0x33]]),
59
+ // 08 or FF = 192 punches, 00 or C1 normal
60
+ memoryOverflow: new storage.SiInt([[0x3d]]),
61
+ // overflow if != 00
62
+ lastWriteDate: new siProtocol.SiDate(6, (i) => 0x75 + i),
63
+ autoOffTimeout: new storage.SiInt([[0x7f], [0x7e]]),
64
+ refreshRate: new storage.SiInt([[0x10]]),
65
+ // in 3/sec ???
66
+ powerMode: new storage.SiInt([[0x11]]),
67
+ // 06 low power, 08 standard/sprint
68
+ interval: new storage.SiInt([[0x49], [0x48]]),
69
+ // in 32*ms
70
+ wtf: new storage.SiInt([[0x4b], [0x4a]]),
71
+ // in 32*ms
72
+ program: new storage.SiInt([[0x70]]),
73
+ // xx0xxxxxb competition, xx1xxxxxb training
74
+ handshake: new storage.SiBool(0x74, 2),
75
+ sprint4ms: new storage.SiBool(0x74, 3),
76
+ passwordOnly: new storage.SiBool(0x74, 4),
77
+ stopOnFullBackup: new storage.SiBool(0x74, 5),
78
+ autoReadout: new storage.SiBool(0x74, 7),
79
+ // depends on autoSend
80
+ sleepDay: new storage.SiInt([[0x7b]]),
81
+ // xxxxxxx0b - seconds relative to midnight/midday: 0 = am, 1 = pm
82
+ // xxxx000xb - day of week: 000 = Sunday, 110 = Saturday
83
+ // xx00xxxxb - week counter 0..3, relative to programming date
84
+ sleepSeconds: new storage.SiInt([[0x7d], [0x7c]]),
85
+ workingMinutes: new storage.SiInt([[0x7f], [0x7e]])
86
+ };
87
+ export const siStationStorageDefinition = storage.defineStorage(0x80, siStationStorageLocations);
88
+ // export type ISiStationStorageFields = storage.FieldsFromStorageDefinition<typeof siStationStorageDefinition>;
89
+ // export type ISiStationStorageFields = storage.FieldsFromStorageLocations<typeof siStationStorageLocations>;
90
+
91
+ export abstract class BaseSiStation<T extends SiTargetMultiplexerTarget> {
92
+ public storage: storage.ISiStorage<ISiStationStorageFields>;
93
+
94
+ protected static fromSiTargetMultiplexerWithGivenTarget<U extends SiTargetMultiplexerTarget>(
95
+ multiplexer: ISiTargetMultiplexer,
96
+ multiplexerTarget: U,
97
+ createNewInstance: () => ISiStation<U>
98
+ ): ISiStation<U> {
99
+ const existingStationObject = multiplexer.stations[multiplexerTarget] as ISiStation<U> | undefined;
100
+ if (existingStationObject) {
101
+ return existingStationObject;
102
+ }
103
+ const instance = createNewInstance();
104
+ // @ts-ignore
105
+ multiplexer.stations[multiplexerTarget] = instance;
106
+ // TODO: deregister/close
107
+ return instance;
108
+ }
109
+
110
+ constructor(public siTargetMultiplexer: ISiTargetMultiplexer, public readonly multiplexerTarget: T) {
111
+ this.storage = siStationStorageDefinition();
112
+ }
113
+
114
+ get ident(): string {
115
+ const multiplexerTargetString = SiTargetMultiplexerTarget[this.multiplexerTarget];
116
+ const deviceIdentString = this.siTargetMultiplexer.siDevice.ident;
117
+ return `${multiplexerTargetString}-${deviceIdentString}`;
118
+ }
119
+
120
+ sendMessage(message: siProtocol.SiMessage, numResponses = 0, timeoutInMiliseconds = 10000): Promise<number[][]> {
121
+ return this.siTargetMultiplexer.sendMessage(this.multiplexerTarget, message, numResponses, timeoutInMiliseconds);
122
+ }
123
+
124
+ readInfo(): Promise<void> {
125
+ return this.sendMessage(
126
+ {
127
+ command: proto.cmd.GET_SYS_VAL,
128
+ parameters: [0x00, 0x80]
129
+ },
130
+ 1
131
+ ).then((data) => {
132
+ this.storage.splice(0x00, 0x80, ...data[0].slice(3));
133
+ });
134
+ }
135
+
136
+ getField<Field extends keyof ISiStationStorageFields>(infoName: Field): storage.ISiStorageLocations<ISiStationStorageFields>[Field] {
137
+ return this.storage.locations[infoName];
138
+ }
139
+
140
+ getInfo<Field extends keyof ISiStationStorageFields>(infoName: Field): storage.ISiFieldValue<ISiStationStorageFields[Field]> | undefined {
141
+ return this.storage.get(infoName);
142
+ }
143
+
144
+ setInfo<Field extends keyof ISiStationStorageFields>(infoName: Field, newValue: storage.ISiFieldValue<ISiStationStorageFields[Field]> | ISiStationStorageFields[Field]): void {
145
+ this.storage.set(infoName, newValue);
146
+ }
147
+
148
+ writeChanges(): Promise<void> {
149
+ const newStorage = this.storage.data;
150
+ return this.readInfo().then(() => {
151
+ const oldStorage = this.storage.data;
152
+ return this.writeDiff(oldStorage, newStorage);
153
+ });
154
+ }
155
+
156
+ atomically(doThings: () => void): Promise<void> {
157
+ return this.readInfo().then(() => {
158
+ const oldStorage = this.storage.data;
159
+ doThings();
160
+ const newStorage = this.storage.data;
161
+ return this.writeDiff(oldStorage, newStorage);
162
+ });
163
+ }
164
+
165
+ writeDiff(oldStorage: storage.ISiStorageData, newStorage: storage.ISiStorageData): Promise<void> {
166
+ const zippedStorageBytes = oldStorage.zip(newStorage);
167
+ const isByteDirty = zippedStorageBytes.map((oldAndNew: [unknown, unknown]) => oldAndNew[0] !== oldAndNew[1]);
168
+ const dirtyRanges = isByteDirty.reduce((ranges: [number, number][], isDirty: boolean, byteIndex: number): [number, number][] => {
169
+ if (!isDirty) {
170
+ return ranges;
171
+ }
172
+ const numRanges = ranges.length;
173
+ const lastRange = ranges[numRanges - 1];
174
+ if (lastRange && lastRange[1] === byteIndex) {
175
+ return [...ranges.slice(0, numRanges - 1), [lastRange[0], byteIndex + 1]];
176
+ }
177
+ return [...ranges, [byteIndex, byteIndex + 1]];
178
+ }, [] as [number, number][]);
179
+ let dirtyRangeIndex = 0;
180
+ const processDirtyRanges = (): Promise<void> => {
181
+ if (dirtyRangeIndex >= dirtyRanges.length) {
182
+ return Promise.resolve();
183
+ }
184
+ const dirtyRange = dirtyRanges[dirtyRangeIndex];
185
+ const parameters = [dirtyRange[0], ...newStorage.slice(dirtyRange[0], dirtyRange[1])] as number[];
186
+ return this.sendMessage(
187
+ {
188
+ command: proto.cmd.SET_SYS_VAL,
189
+ parameters: parameters
190
+ },
191
+ 1
192
+ ).then((d) => {
193
+ const data = d[0];
194
+ data.splice(0, 2);
195
+ if (data[0] !== parameters[0]) {
196
+ console.warn(`SET_SYS_VAL error: ${data[0]} (expected ${parameters[0]})`);
197
+ }
198
+ dirtyRangeIndex += 1;
199
+ return processDirtyRanges();
200
+ });
201
+ };
202
+ return processDirtyRanges();
203
+ }
204
+
205
+
206
+
207
+ getTime(): Promise<Date | undefined> {
208
+ return this.sendMessage(
209
+ {
210
+ command: proto.cmd.GET_TIME,
211
+ parameters: []
212
+ },
213
+ 1
214
+ ).then((d) => siProtocol.arr2date(d[0].slice(2)));
215
+ }
216
+
217
+ setTime(newTime: Date): Promise<Date | undefined> {
218
+ // TODO: compensate for waiting time
219
+ return this.sendMessage(
220
+ {
221
+ command: proto.cmd.SET_TIME,
222
+ parameters: [...siProtocol.date2arr(newTime)]
223
+ },
224
+ 1
225
+ ).then((d) => siProtocol.arr2date(d[0].slice(2)));
226
+ }
227
+
228
+ signal(countArg: number): Promise<void> {
229
+ const count = countArg < 1 ? 1 : countArg;
230
+ return this.sendMessage(
231
+ {
232
+ command: proto.cmd.SIGNAL,
233
+ parameters: [count]
234
+ },
235
+ 1
236
+ ).then((data) => {
237
+ if (data[0][2] !== count) {
238
+ throw new Error('NUM BEEPS');
239
+ }
240
+ });
241
+ }
242
+
243
+ powerOff(): Promise<number[][]> {
244
+ // Does not power off BSM8 (USB powered), though
245
+ return this.sendMessage(
246
+ {
247
+ command: proto.cmd.OFF,
248
+ parameters: []
249
+ },
250
+ 0
251
+ );
252
+ }
253
+ }
@@ -0,0 +1,41 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+ import * as testUtils from '../testUtils';
3
+ import type { ISiDevice, ISiDeviceDriverData } from '../SiDevice/ISiDevice';
4
+ import type { ISiDeviceDriver } from '../SiDevice/ISiDeviceDriver';
5
+ import { SiDevice } from '../SiDevice/SiDevice';
6
+ import { SiTargetMultiplexerTarget } from './ISiTargetMultiplexer';
7
+ import { CoupledSiStation } from './CoupledSiStation';
8
+ import { SiTargetMultiplexer } from './SiTargetMultiplexer';
9
+
10
+ testUtils.useFakeTimers();
11
+
12
+ function mockDriver(driver: Partial<ISiDeviceDriver<ISiDeviceDriverData<unknown>>>) {
13
+ return driver as unknown as ISiDeviceDriver<ISiDeviceDriverData<unknown>>;
14
+ }
15
+
16
+ describe('CoupledSiStation', () => {
17
+ test('fromSiDevice', () => {
18
+ const fakeSiDevice = new SiDevice('fromSiDevice', {
19
+ driver: mockDriver({ name: 'FakeSiDevice' })
20
+ });
21
+ const myCoupledStation1 = CoupledSiStation.fromSiDevice(fakeSiDevice);
22
+ expect(myCoupledStation1 instanceof CoupledSiStation).toBe(true);
23
+ expect(myCoupledStation1.ident).toBe('Remote-FakeSiDevice-fromSiDevice');
24
+ expect(myCoupledStation1.multiplexerTarget).toBe(SiTargetMultiplexerTarget.Remote);
25
+ const myCoupledStation2 = CoupledSiStation.fromSiDevice(fakeSiDevice);
26
+ expect(myCoupledStation2).toBe(myCoupledStation1);
27
+ expect(myCoupledStation2.ident).toBe('Remote-FakeSiDevice-fromSiDevice');
28
+ expect(myCoupledStation2.multiplexerTarget).toBe(SiTargetMultiplexerTarget.Remote);
29
+ });
30
+ test('fromSiTargetMultiplexer', () => {
31
+ const myTargetMultiplexer = new SiTargetMultiplexer({ ident: 'fake-ident' } as ISiDevice<ISiDeviceDriverData<unknown>>);
32
+ const myCoupledStation1 = CoupledSiStation.fromSiTargetMultiplexer(myTargetMultiplexer);
33
+ expect(myCoupledStation1 instanceof CoupledSiStation).toBe(true);
34
+ expect(myCoupledStation1.ident).toBe('Remote-fake-ident');
35
+ expect(myCoupledStation1.multiplexerTarget).toBe(SiTargetMultiplexerTarget.Remote);
36
+ const myCoupledStation2 = CoupledSiStation.fromSiTargetMultiplexer(myTargetMultiplexer);
37
+ expect(myCoupledStation2).toBe(myCoupledStation1);
38
+ expect(myCoupledStation2.ident).toBe('Remote-fake-ident');
39
+ expect(myCoupledStation2.multiplexerTarget).toBe(SiTargetMultiplexerTarget.Remote);
40
+ });
41
+ });
@@ -0,0 +1,130 @@
1
+ import type { ISiDevice, ISiDeviceDriverData } from '../SiDevice/ISiDevice';
2
+ import type { ISiStation } from './ISiStation';
3
+ import { type ISiTargetMultiplexer, SiTargetMultiplexerTarget } from './ISiTargetMultiplexer';
4
+ import { BaseSiStation } from './BaseSiStation';
5
+ import { SiTargetMultiplexer } from './SiTargetMultiplexer';
6
+ import { proto } from '../constants';
7
+ import * as siProtocol from '../siProtocol';
8
+
9
+ export class CoupledSiStation extends BaseSiStation<SiTargetMultiplexerTarget.Remote> implements ISiStation<SiTargetMultiplexerTarget.Remote> {
10
+ static fromSiDevice(siDevice: ISiDevice<ISiDeviceDriverData<unknown>>): CoupledSiStation {
11
+ const multiplexer = SiTargetMultiplexer.fromSiDevice(siDevice);
12
+ return this.fromSiTargetMultiplexer(multiplexer);
13
+ }
14
+
15
+ static fromSiTargetMultiplexer(multiplexer: ISiTargetMultiplexer): CoupledSiStation {
16
+ return this.fromSiTargetMultiplexerWithGivenTarget(multiplexer, SiTargetMultiplexerTarget.Remote, () => new this(multiplexer, SiTargetMultiplexerTarget.Remote)) as CoupledSiStation;
17
+ }
18
+
19
+ async getBackupData(turnOff:boolean=true): Promise<{ code: number, cardNumber: number|undefined ; date: Date | undefined}[]> {
20
+ let backupData: { code: number, cardNumber: number|undefined ; date: Date | undefined}[] = []
21
+ let backupNextWritePointer:number = 0
22
+
23
+ // Get backup pointer
24
+ for(let tries = 0; tries<10 && (backupNextWritePointer == 0 || backupNextWritePointer == undefined); tries++){
25
+ try{
26
+ await this.siTargetMultiplexer.sendMessage(
27
+ SiTargetMultiplexerTarget.Remote,
28
+ {
29
+ command: proto.cmd.GET_SYS_VAL,
30
+ parameters: [0x1c,0x07]
31
+ },
32
+ 1, 10000).then((d) => {
33
+ backupNextWritePointer = (d[0][3]<<24) | (d[0][4]<<16) | (d[0][8]<<8) | d[0][9]
34
+ }).catch(async (e) => {
35
+ await this.siTargetMultiplexer.sendMessage(
36
+ SiTargetMultiplexerTarget.Remote,
37
+ {
38
+ command: proto.WAKEUP,
39
+ parameters: []
40
+ },
41
+ 0, 10000)
42
+ await new Promise( resolve => setTimeout(resolve, 500) );
43
+ })
44
+ }catch (e){ // ignore, try again
45
+ }
46
+ }
47
+ if(backupNextWritePointer == 0 || backupNextWritePointer == undefined){
48
+ return Promise.reject(new Error("Unable to access coupled si station!"))
49
+ }
50
+
51
+
52
+ let backupReadLocation = 0x0100
53
+ let backupMaxLocation = 0x200000
54
+ let backupStorageSize = 128
55
+ while(backupReadLocation < backupNextWritePointer && backupReadLocation < backupMaxLocation){
56
+ try{
57
+ const actualReadLength = Math.min(backupStorageSize,backupNextWritePointer-backupReadLocation)
58
+ await this.siTargetMultiplexer.sendMessage(
59
+ SiTargetMultiplexerTarget.Remote,
60
+ {
61
+ command: proto.cmd.GET_BACKUP,
62
+ parameters: [(backupReadLocation>>16)&0xff,(backupReadLocation>>8)&0xff,backupReadLocation&0xff,actualReadLength]
63
+ },
64
+ 1, 10000)
65
+ .then((d) => {
66
+ let cn = (d[0][0]<<8)|d[0][1]
67
+ let addr = (d[0][2]<<16)|(d[0][3]<<8)|d[0][4]
68
+ let p = 5
69
+ while (p<d[0].length){
70
+ let sicard = siProtocol.arr2cardNumber([d[0][p+2],d[0][p+1],d[0][p+0]])
71
+ let datedata = [
72
+ d[0][p+3]>>2, // Year = bit 7-2
73
+ (((d[0][p+3]&0x3)<<2)|((d[0][p+4]>>6)&0x3)), // Month = bit 1-0 and 7-6
74
+ ((d[0][p+4]>>1)&0x1F), // day = bit 5-1
75
+ (d[0][p+4]&0x1), // am/pm halfday = 0 bit
76
+ d[0][p+5],
77
+ d[0][p+6],
78
+ d[0][p+7]
79
+ ]
80
+ let date = siProtocol.arr2date(datedata)
81
+ if(addr+p<=backupNextWritePointer){
82
+ backupData.push({code:cn,cardNumber:sicard,date:date})
83
+ }else{
84
+ break
85
+ }
86
+ p+=proto.REC_LEN
87
+ }
88
+ backupReadLocation += actualReadLength
89
+ })
90
+ }catch(e){
91
+ if(backupStorageSize >= 2*proto.REC_LEN){
92
+ backupStorageSize/=2
93
+ console.log("reduce requested backup block size to", backupStorageSize)
94
+ }else{
95
+ return Promise.reject("Unable to read backup data")
96
+ }
97
+ }
98
+
99
+ }
100
+
101
+ // Confirm read end with signal
102
+ await this.siTargetMultiplexer.sendMessage(
103
+ SiTargetMultiplexerTarget.Remote,
104
+ {
105
+ command: proto.cmd.SIGNAL,
106
+ parameters: [0x2]
107
+ }
108
+ )
109
+ // Turn of unit if requested
110
+ if(turnOff){
111
+ await new Promise( resolve => setTimeout(resolve, 250) );
112
+ for(let tries = 0, hasTurnedOff=false; tries<5&&!hasTurnedOff; tries++){
113
+ await this.siTargetMultiplexer.sendMessage(
114
+ SiTargetMultiplexerTarget.Remote,
115
+ {
116
+ command: proto.cmd.OFF,
117
+ parameters: []
118
+ }
119
+ ,1,10000
120
+ ).then(()=>{
121
+ hasTurnedOff = true
122
+ }).catch(e=>{
123
+ hasTurnedOff = false
124
+ })
125
+ }
126
+ }
127
+
128
+ return backupData
129
+ }
130
+ }
@@ -0,0 +1,29 @@
1
+ import type { ISiCard } from '../SiCard/ISiCard';
2
+ import * as utils from '../utils';
3
+
4
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
5
+ export interface ISiMainStation extends utils.IEventTarget<SiMainStationEvents> {}
6
+
7
+ export class SiMainStationSiCardInsertedEvent extends utils.Event<'siCardInserted'> {
8
+ constructor(public siMainStation: ISiMainStation, public siCard: ISiCard) {
9
+ super();
10
+ }
11
+ }
12
+
13
+ export class SiMainStationSiCardRemovedEvent extends utils.Event<'siCardRemoved'> {
14
+ constructor(public siMainStation: ISiMainStation, public siCard: ISiCard) {
15
+ super();
16
+ }
17
+ }
18
+
19
+ export class SiMainStationSiCardObservedEvent extends utils.Event<'siCardObserved'> {
20
+ constructor(public siMainStation: ISiMainStation, public siCard: ISiCard) {
21
+ super();
22
+ }
23
+ }
24
+
25
+ export type SiMainStationEvents = {
26
+ siCardInserted: SiMainStationSiCardInsertedEvent;
27
+ siCardRemoved: SiMainStationSiCardRemovedEvent;
28
+ siCardObserved: SiMainStationSiCardObservedEvent;
29
+ };
@@ -0,0 +1,9 @@
1
+ /* eslint-disable no-unused-vars,no-shadow */
2
+ export enum SiSendTaskState {
3
+ Queued = 0,
4
+ Sending = 1,
5
+ Sent = 2,
6
+ Succeeded = 3,
7
+ Failed = 4
8
+ }
9
+ /* eslint-enable no-unused-vars,no-shadow */