motion-master-client 0.0.55 → 0.0.56

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 (132) hide show
  1. package/.babelrc +3 -0
  2. package/.eslintrc.json +18 -0
  3. package/README.md +123 -123
  4. package/jest.config.ts +16 -0
  5. package/motion-master.proto +1861 -0
  6. package/package.json +6 -21
  7. package/project.json +45 -0
  8. package/src/{index.d.ts → index.ts} +26 -26
  9. package/src/lib/cia402.spec.ts +77 -0
  10. package/src/lib/cia402.ts +414 -0
  11. package/src/lib/config-file.spec.ts +114 -0
  12. package/src/lib/config-file.ts +63 -0
  13. package/src/lib/device-log-line.ts +5 -0
  14. package/src/lib/device-parameter.spec.ts +85 -0
  15. package/src/lib/device-parameter.ts +79 -0
  16. package/src/lib/device.ts +10 -0
  17. package/src/lib/hardware-description.spec.ts +253 -0
  18. package/src/lib/hardware-description.ts +129 -0
  19. package/src/lib/logger.ts +5 -0
  20. package/src/lib/monitoring-config.ts +6 -0
  21. package/src/lib/{monitoring-entry.d.ts → monitoring-entry.ts} +10 -9
  22. package/src/lib/motion-master-client.ts +266 -0
  23. package/src/lib/motion-master-pub-sub-client.ts +100 -0
  24. package/src/lib/motion-master-pub-sub-socket.ts +46 -0
  25. package/src/lib/motion-master-pub-sub-web-socket.ts +77 -0
  26. package/src/lib/motion-master-pub-sub-worker-socket.ts +57 -0
  27. package/src/lib/motion-master-req-res-client.spec.ts +740 -0
  28. package/src/lib/motion-master-req-res-client.ts +2211 -0
  29. package/src/lib/motion-master-req-res-socket.ts +68 -0
  30. package/src/lib/motion-master-req-res-web-socket.ts +123 -0
  31. package/src/lib/motion-master-req-res-worker-socket.ts +89 -0
  32. package/src/lib/motion-master.proto.d.ts +5183 -5083
  33. package/src/lib/motion-master.proto.js +53218 -52284
  34. package/src/lib/operators.ts +108 -0
  35. package/src/lib/options.ts +12 -0
  36. package/src/lib/parameter.spec.ts +160 -0
  37. package/src/lib/parameter.ts +170 -0
  38. package/src/lib/product-id-range.ts +8 -0
  39. package/src/lib/request-status-resolver.ts +403 -0
  40. package/src/lib/system-log-line.ts +9 -0
  41. package/src/lib/{types.d.ts → types.ts} +141 -209
  42. package/src/lib/urls.ts +6 -0
  43. package/src/lib/util.ts +332 -0
  44. package/src/lib/web-socket-connection-close-codes.ts +85 -0
  45. package/tsconfig.json +23 -0
  46. package/tsconfig.lib.json +10 -0
  47. package/tsconfig.spec.json +20 -0
  48. package/typedoc.json +10 -0
  49. package/src/index.js +0 -30
  50. package/src/index.js.map +0 -1
  51. package/src/lib/cia402.d.ts +0 -182
  52. package/src/lib/cia402.js +0 -392
  53. package/src/lib/cia402.js.map +0 -1
  54. package/src/lib/config-file.d.ts +0 -13
  55. package/src/lib/config-file.js +0 -50
  56. package/src/lib/config-file.js.map +0 -1
  57. package/src/lib/device-log-line.d.ts +0 -5
  58. package/src/lib/device-log-line.js +0 -3
  59. package/src/lib/device-log-line.js.map +0 -1
  60. package/src/lib/device-parameter.d.ts +0 -56
  61. package/src/lib/device-parameter.js +0 -39
  62. package/src/lib/device-parameter.js.map +0 -1
  63. package/src/lib/device.d.ts +0 -9
  64. package/src/lib/device.js +0 -3
  65. package/src/lib/device.js.map +0 -1
  66. package/src/lib/hardware-description.d.ts +0 -41
  67. package/src/lib/hardware-description.js +0 -94
  68. package/src/lib/hardware-description.js.map +0 -1
  69. package/src/lib/logger.d.ts +0 -1
  70. package/src/lib/logger.js +0 -8
  71. package/src/lib/logger.js.map +0 -1
  72. package/src/lib/monitoring-config.d.ts +0 -6
  73. package/src/lib/monitoring-config.js +0 -3
  74. package/src/lib/monitoring-config.js.map +0 -1
  75. package/src/lib/monitoring-entry.js +0 -3
  76. package/src/lib/monitoring-entry.js.map +0 -1
  77. package/src/lib/motion-master-client.d.ts +0 -56
  78. package/src/lib/motion-master-client.js +0 -167
  79. package/src/lib/motion-master-client.js.map +0 -1
  80. package/src/lib/motion-master-pub-sub-client.d.ts +0 -17
  81. package/src/lib/motion-master-pub-sub-client.js +0 -73
  82. package/src/lib/motion-master-pub-sub-client.js.map +0 -1
  83. package/src/lib/motion-master-pub-sub-socket.d.ts +0 -42
  84. package/src/lib/motion-master-pub-sub-socket.js +0 -3
  85. package/src/lib/motion-master-pub-sub-socket.js.map +0 -1
  86. package/src/lib/motion-master-pub-sub-web-socket.d.ts +0 -18
  87. package/src/lib/motion-master-pub-sub-web-socket.js +0 -66
  88. package/src/lib/motion-master-pub-sub-web-socket.js.map +0 -1
  89. package/src/lib/motion-master-pub-sub-worker-socket.d.ts +0 -18
  90. package/src/lib/motion-master-pub-sub-worker-socket.js +0 -48
  91. package/src/lib/motion-master-pub-sub-worker-socket.js.map +0 -1
  92. package/src/lib/motion-master-req-res-client.d.ts +0 -947
  93. package/src/lib/motion-master-req-res-client.js +0 -1735
  94. package/src/lib/motion-master-req-res-client.js.map +0 -1
  95. package/src/lib/motion-master-req-res-socket.d.ts +0 -60
  96. package/src/lib/motion-master-req-res-socket.js +0 -3
  97. package/src/lib/motion-master-req-res-socket.js.map +0 -1
  98. package/src/lib/motion-master-req-res-web-socket.d.ts +0 -28
  99. package/src/lib/motion-master-req-res-web-socket.js +0 -98
  100. package/src/lib/motion-master-req-res-web-socket.js.map +0 -1
  101. package/src/lib/motion-master-req-res-worker-socket.d.ts +0 -24
  102. package/src/lib/motion-master-req-res-worker-socket.js +0 -72
  103. package/src/lib/motion-master-req-res-worker-socket.js.map +0 -1
  104. package/src/lib/operators.d.ts +0 -20
  105. package/src/lib/operators.js +0 -84
  106. package/src/lib/operators.js.map +0 -1
  107. package/src/lib/options.d.ts +0 -10
  108. package/src/lib/options.js +0 -14
  109. package/src/lib/options.js.map +0 -1
  110. package/src/lib/parameter.d.ts +0 -72
  111. package/src/lib/parameter.js +0 -119
  112. package/src/lib/parameter.js.map +0 -1
  113. package/src/lib/product-id-range.d.ts +0 -7
  114. package/src/lib/product-id-range.js +0 -12
  115. package/src/lib/product-id-range.js.map +0 -1
  116. package/src/lib/request-status-resolver.d.ts +0 -4
  117. package/src/lib/request-status-resolver.js +0 -345
  118. package/src/lib/request-status-resolver.js.map +0 -1
  119. package/src/lib/system-log-line.d.ts +0 -9
  120. package/src/lib/system-log-line.js +0 -3
  121. package/src/lib/system-log-line.js.map +0 -1
  122. package/src/lib/types.js +0 -29
  123. package/src/lib/types.js.map +0 -1
  124. package/src/lib/urls.d.ts +0 -3
  125. package/src/lib/urls.js +0 -10
  126. package/src/lib/urls.js.map +0 -1
  127. package/src/lib/util.d.ts +0 -42
  128. package/src/lib/util.js +0 -327
  129. package/src/lib/util.js.map +0 -1
  130. package/src/lib/web-socket-connection-close-codes.d.ts +0 -8
  131. package/src/lib/web-socket-connection-close-codes.js +0 -89
  132. package/src/lib/web-socket-connection-close-codes.js.map +0 -1
@@ -0,0 +1,129 @@
1
+ import { isEqual } from 'lodash';
2
+ import semver from 'semver';
3
+
4
+ export interface HardwareComponent {
5
+ name: string;
6
+ version: string;
7
+ serialNumber: string;
8
+ }
9
+
10
+ export type AssemblyComponent = HardwareComponent;
11
+ export type DeviceComponent = HardwareComponent;
12
+
13
+ export interface Assembly {
14
+ name: string;
15
+ id: string;
16
+ version: string;
17
+ serialNumber: string;
18
+ components: AssemblyComponent[];
19
+ }
20
+
21
+ export interface HardwareDevice {
22
+ name: string;
23
+ id: string;
24
+ version: string;
25
+ keyId: string;
26
+ serialNumber: string;
27
+ macAddress: string;
28
+ components: DeviceComponent[];
29
+ }
30
+
31
+ /**
32
+ * @link https://docs.google.com/document/d/1N00-ZjxHkjoWNIsnoTtza9CUHrxm8-e54298DXLuD94
33
+ */
34
+ export interface HardwareDescription {
35
+ fileVersion: string;
36
+ assembly?: Assembly;
37
+ device: HardwareDevice;
38
+ }
39
+
40
+ export function getIdFromHardwareDescription(hd: HardwareDescription): string {
41
+ return hd.assembly
42
+ ? hd.assembly.id
43
+ : hd.device.id;
44
+ }
45
+
46
+ export function getApiIdentifierFromHardwareDescription(hd: HardwareDescription, skipKeyId = false): string {
47
+ let apiId = hd.assembly
48
+ ? `${hd.assembly.id}-${hd.assembly.version}`
49
+ : `${hd.device.id}-${hd.device.version}`;
50
+
51
+ if (!skipKeyId && hd.device.keyId) {
52
+ apiId += `-${hd.device.keyId}`;
53
+ }
54
+
55
+ return apiId;
56
+ }
57
+
58
+ export function getNameFromHardwareDescription(hd: HardwareDescription): string {
59
+ return hd.assembly
60
+ ? hd.assembly.name
61
+ : hd.device.name;
62
+ }
63
+
64
+ export function getSerialNumberFromHardwareDescription(hd: HardwareDescription): string {
65
+ return hd.assembly
66
+ ? hd.assembly.serialNumber
67
+ : hd.device.serialNumber;
68
+ }
69
+
70
+ export function getAllComponentsFromHardwareDescription(hd: HardwareDescription): HardwareComponent[] {
71
+ return hd.assembly
72
+ ? [...hd.assembly.components, ...hd.device.components]
73
+ : hd.device.components;
74
+ }
75
+
76
+ export function hardwareComponentsMatch(a: HardwareComponent[], b: HardwareComponent[]): boolean {
77
+ if (a.length === 0 && b.length === 0) {
78
+ return false; // edge case: when both components are empty they don't match since there are no components to compare
79
+ }
80
+
81
+ const ac = a.map((c) => `${c.name}-${c.version[0]}`.replace(/\s/g, '').toLocaleLowerCase()).sort();
82
+ const bc = b.map((c) => `${c.name}-${c.version[0]}`.replace(/\s/g, '').toLocaleLowerCase()).sort();
83
+
84
+ return isEqual(ac, bc);
85
+ }
86
+
87
+ export function isHardwareDescriptionCompatibleWithPackageFilename(hardwareDescription: HardwareDescription, packageFilename: string): boolean {
88
+ const hdApiId = getApiIdentifierFromHardwareDescription(hardwareDescription);
89
+ const pfApiId = getApiIdentifierFromPackageFilename(packageFilename);
90
+
91
+ if (hdApiId && pfApiId) {
92
+ return hdApiId === pfApiId;
93
+ } else {
94
+ const pfComponents = getHardwareComponentsFromPackageFilename(packageFilename);
95
+ const hdComponents = getAllComponentsFromHardwareDescription(hardwareDescription);
96
+ return hardwareComponentsMatch(pfComponents, hdComponents);
97
+ }
98
+ }
99
+
100
+ export function getHardwareComponentsFromPackageFilename(packageFilename: string): HardwareComponent[] {
101
+ if (packageFilename.match(/_Com[^_]+_Core[^_]+_Drive/g)) {
102
+ return packageFilename.split('_').slice(1, 4).map((b) => { // ['ComEtherCAT-b', 'CoreC2X-a', 'Drive400-d']
103
+ const p = b.split('-'); // ['ComEtherCAT', 'b']
104
+ return {
105
+ name: p[0],
106
+ serialNumber: '',
107
+ version: p[1],
108
+ } as HardwareComponent;
109
+ });
110
+ } else {
111
+ // not all package filenames have components,
112
+ // e.g. 'package_SOMANET-Servo-Node-Circulo-1800_circulo1800_motion-drive_v4.2.0-beta.5.zip'
113
+ return [];
114
+ }
115
+ }
116
+
117
+ export function getVersionFromPackageFilename(packageFilename: string) {
118
+ const parts = packageFilename.split('_');
119
+ const version = parts[parts.length - 1].replace(/\.zip$/, '').split(/\s+/)[0];
120
+ return semver.valid(version) ? version : undefined;
121
+ }
122
+
123
+ export function getApiIdentifierFromPackageFilename(packageFilename: string) {
124
+ if (packageFilename.match(/_Com[^_]+_Core[^_]+_Drive/g)) {
125
+ return undefined;
126
+ }
127
+ const [, , apiIdentifier] = packageFilename.split('_');
128
+ return apiIdentifier;
129
+ }
@@ -0,0 +1,5 @@
1
+ import { Roarr } from 'roarr';
2
+
3
+ export const logger = Roarr.child({
4
+ package: '@synapticon/motion-master-client'
5
+ });
@@ -0,0 +1,6 @@
1
+ export interface MonitoringConfig {
2
+ readonly bufferSize?: number;
3
+ readonly distinct?: boolean;
4
+ readonly messageId?: string;
5
+ readonly topic: string;
6
+ }
@@ -1,9 +1,10 @@
1
- import { Observable } from "rxjs";
2
- import { MonitoringConfig } from "./monitoring-config";
3
- import { MotionMasterMessage, ParameterValueType } from "./types";
4
- export interface MonitoringEntry {
5
- config: Required<MonitoringConfig>;
6
- interval: number;
7
- values$: Observable<ParameterValueType[]>;
8
- parameters: MotionMasterMessage.Request.GetDeviceParameterValues.IParameter[];
9
- }
1
+ import { Observable } from "rxjs";
2
+ import { MonitoringConfig } from "./monitoring-config";
3
+ import { MotionMasterMessage, ParameterValueType } from "./types";
4
+
5
+ export interface MonitoringEntry {
6
+ config: Required<MonitoringConfig>;
7
+ interval: number;
8
+ values$: Observable<ParameterValueType[]>;
9
+ parameters: MotionMasterMessage.Request.GetDeviceParameterValues.IParameter[];
10
+ }
@@ -0,0 +1,266 @@
1
+ import { combineLatest, defer, distinctUntilChanged, filter, first, firstValueFrom, map, mergeMap, Observable, Subscription, timeout } from "rxjs";
2
+ import { v4 } from "uuid";
3
+ import { MonitoringConfig } from "./monitoring-config";
4
+ import { MotionMasterPubSubClient } from "./motion-master-pub-sub-client";
5
+ import { MotionMasterPubSubSocket } from "./motion-master-pub-sub-socket";
6
+ import { MotionMasterPubSubWebSocket } from "./motion-master-pub-sub-web-socket";
7
+ import { MotionMasterReqResClient } from "./motion-master-req-res-client";
8
+ import { MotionMasterReqResSocket } from "./motion-master-req-res-socket";
9
+ import { MotionMasterReqResWebSocket } from "./motion-master-req-res-web-socket";
10
+ import { mapMonitoringParameterValuesStatusMessageToDeviceParameters, mapMonitoringParameterValuesStatusMessageToParameterValues, selectMotionMasterMessageByTopic } from "./operators";
11
+ import { DeviceParameterIds, DeviceRef, MotionMasterMessage, ParameterValueType } from "./types";
12
+ import { DeviceParameter } from "./device-parameter";
13
+
14
+ export function createMotionMasterClient(hostname: string) {
15
+ const reqResSocket = new MotionMasterReqResWebSocket();
16
+ const pubSubSocket = new MotionMasterPubSubWebSocket();
17
+
18
+ const client = new MotionMasterClient(reqResSocket, pubSubSocket);
19
+
20
+ const clientId = v4();
21
+ reqResSocket.open(`ws://${hostname}:63524?clientId=${clientId}`);
22
+ pubSubSocket.open(`ws://${hostname}:63525?clientId=${clientId}`);
23
+
24
+ return client;
25
+ }
26
+
27
+ export class MotionMasterClient {
28
+
29
+ readonly request: MotionMasterReqResClient;
30
+ readonly monitor: MotionMasterPubSubClient;
31
+
32
+ private systemEventSubscription?: Subscription;
33
+
34
+ constructor(
35
+ readonly reqResSocket: MotionMasterReqResSocket,
36
+ readonly pubSubSocket: MotionMasterPubSubSocket,
37
+ ) {
38
+ this.request = new MotionMasterReqResClient(this.reqResSocket);
39
+ this.monitor = new MotionMasterPubSubClient(this.pubSubSocket);
40
+
41
+ this.subscribeToSystemEvent();
42
+ }
43
+
44
+ /**
45
+ * This observable will emit true when both req/res and pub/sub sockets are opened.
46
+ * If any socket gets closed, this observable will emit false.
47
+ * This is a hot observable that will on subscription emit the last value of sockets opened.
48
+ */
49
+ socketsOpened$ = combineLatest([this.reqResSocket.opened$, this.pubSubSocket.opened$]).pipe(
50
+ map(([reqResSocketOpened, pubSubSocketOpened]) => reqResSocketOpened && pubSubSocketOpened),
51
+ );
52
+
53
+ /**
54
+ * Client is ready when both req/res and pub/sub sockets are opened.
55
+ * This observable will emit whenever the sockets opened property changes to true.
56
+ */
57
+ ready$ = this.socketsOpened$.pipe(
58
+ filter(Boolean),
59
+ );
60
+
61
+ /**
62
+ * This observable will emit only once when both req/res and pub/sub sockets are opened.
63
+ * NOTE: This observable might never emit and it doesn't timeout.
64
+ */
65
+ onceReady$ = this.ready$.pipe(
66
+ first(),
67
+ );
68
+
69
+ /**
70
+ * Client is ready when both req/res and pub/sub sockets are opened.
71
+ * If both sockets are already opened, this function will immediately resolve to true.
72
+ * Otherwise, it will wait up to requestTimeout for both sockets to open, or throw an error.
73
+ */
74
+ whenReady(requestTimeout = 5000): Promise<true> {
75
+ return firstValueFrom(
76
+ this.ready$.pipe(
77
+ timeout(requestTimeout),
78
+ ),
79
+ );
80
+ }
81
+
82
+ startMonitoring(
83
+ ids: DeviceParameterIds,
84
+ monitoringInterval: number,
85
+ config?: MonitoringConfig,
86
+ requestTimeout = 5000,
87
+ ): Observable<ParameterValueType[]> {
88
+ return defer(() => {
89
+ const topic = config?.topic ?? v4();
90
+ const messageId = config?.messageId ?? v4();
91
+
92
+ if (!config) {
93
+ config = { messageId, topic, bufferSize: 1, distinct: false };
94
+ }
95
+
96
+ this.request.resolveGetParameterValuesIds(ids).pipe(
97
+ mergeMap((refs) => {
98
+ if ((new Set(refs.map(([deviceAddress]) => deviceAddress))).size !== 1) {
99
+ throw new Error('Start monitoring only supports a single device');
100
+ }
101
+
102
+ // Skipping write-only (wo) parameters is necessary because Motion Master does not initiate monitoring when the requested list contains such parameters.
103
+ // For now, this serves as a temporary solution until Motion Master is fixed.
104
+ refs = refs.filter(([, index]) => index !== 0x1024);
105
+
106
+ const deviceAddress = refs[0][0];
107
+
108
+ const props: MotionMasterMessage.Request.IStartMonitoringDeviceParameterValues = {
109
+ getDeviceParameterValues: {
110
+ deviceAddress,
111
+ parameters: refs.map(([, index, subindex]) => ({ index, subindex, loadFromCache: false })),
112
+ sendProgress: false,
113
+ },
114
+ interval: monitoringInterval,
115
+ topic,
116
+ };
117
+
118
+ return this.request.startMonitoringDeviceParameterValues(props, requestTimeout, messageId);
119
+ }),
120
+ ).subscribe();
121
+
122
+ this.monitor.subscribe(config);
123
+
124
+ const values$ = this.monitor.socket.data$.pipe(
125
+ selectMotionMasterMessageByTopic(config.topic),
126
+ mapMonitoringParameterValuesStatusMessageToParameterValues(),
127
+ );
128
+
129
+ return new Observable<ParameterValueType[]>((subscriber) => {
130
+ const subscription = values$.subscribe(subscriber);
131
+ return () => {
132
+ subscription.unsubscribe();
133
+ this.stopMonitoring(messageId);
134
+ };
135
+ });
136
+ });
137
+ }
138
+
139
+ /**
140
+ * TODO: return device parameters instead of just parameters
141
+ * get parameters from ESI uprfront, and then when parameters are received
142
+ * from motion master just concatenate (spread operator) props from ESI and props from parameter
143
+ * assign device parameter id like 0x1018:00.12345
144
+ * when assembling device parameter, get parameter from device in ESI based on product code, see ESI service
145
+ * this should probably done for other get parameters functions, get ESI file from device or prepared, get data from it
146
+ */
147
+ startMonitoringDeviceParameters(
148
+ ids: DeviceParameterIds,
149
+ monitoringInterval: number,
150
+ config?: MonitoringConfig,
151
+ requestTimeout = 5000,
152
+ ): Observable<DeviceParameter[]> {
153
+ return defer(() => {
154
+ const topic = config?.topic ?? v4();
155
+ const messageId = config?.messageId ?? v4();
156
+
157
+ if (!config) {
158
+ config = { messageId, topic, bufferSize: 1, distinct: false };
159
+ }
160
+
161
+ this.request.resolveGetParameterValuesIds(ids).pipe(
162
+ mergeMap((refs) => {
163
+ if ((new Set(refs.map(([deviceAddress]) => deviceAddress))).size !== 1) {
164
+ throw new Error('Start monitoring only supports a single device');
165
+ }
166
+
167
+ // Skipping write-only (wo) parameters is necessary because Motion Master does not initiate monitoring when the requested list contains such parameters.
168
+ // For now, this serves as a temporary solution until Motion Master is fixed.
169
+ refs = refs.filter(([, index]) => index !== 0x1024);
170
+
171
+ const deviceAddress = refs[0][0];
172
+
173
+ const props: MotionMasterMessage.Request.IStartMonitoringDeviceParameterValues = {
174
+ getDeviceParameterValues: {
175
+ deviceAddress,
176
+ parameters: refs.map(([, index, subindex]) => ({ index, subindex, loadFromCache: false })),
177
+ sendProgress: false,
178
+ },
179
+ interval: monitoringInterval,
180
+ topic,
181
+ };
182
+
183
+ return this.request.startMonitoringDeviceParameterValues(props, requestTimeout, messageId);
184
+ }),
185
+ ).subscribe();
186
+
187
+ this.monitor.subscribe(config);
188
+
189
+ const values$ = this.monitor.socket.data$.pipe(
190
+ selectMotionMasterMessageByTopic(config.topic),
191
+ mapMonitoringParameterValuesStatusMessageToDeviceParameters(this.request),
192
+ );
193
+
194
+ return new Observable<DeviceParameter[]>((subscriber) => {
195
+ const subscription = values$.subscribe(subscriber);
196
+ return () => {
197
+ subscription.unsubscribe();
198
+ this.stopMonitoring(messageId);
199
+ };
200
+ });
201
+ });
202
+ }
203
+
204
+ startMonitoringValue<T extends ParameterValueType = number>(
205
+ id: string | [DeviceRef, number, number],
206
+ monitoringInterval: number,
207
+ config?: MonitoringConfig,
208
+ requestTimeout = 5000,
209
+ ): Observable<T> {
210
+ const ids = typeof id === 'string' ? [id] : [id];
211
+ return this.startMonitoring(ids, monitoringInterval, config, requestTimeout).pipe(
212
+ map((parameters) => (parameters[0] ?? 0) as T),
213
+ );
214
+ }
215
+
216
+ trackTargetReached(deviceRef: DeviceRef, monitoringInterval = 20): Observable<boolean> {
217
+ return this.startMonitoringValue([deviceRef, 0x6041, 0], monitoringInterval).pipe(
218
+ map((value) => (value & 0x0400) > 0), // tr = target reached bit
219
+ distinctUntilChanged(),
220
+ );
221
+ }
222
+
223
+ waitUntilTargetReached(deviceRef: DeviceRef, monitoringInterval = 20): Observable<true> {
224
+ return this.trackTargetReached(deviceRef, monitoringInterval).pipe(
225
+ filter(Boolean),
226
+ first(),
227
+ );
228
+ }
229
+
230
+ whenTargetReached(deviceRef: DeviceRef, monitoringInterval = 20): Promise<true> {
231
+ return firstValueFrom(this.waitUntilTargetReached(deviceRef, monitoringInterval));
232
+ }
233
+
234
+ subscribeToSystemEvent() {
235
+ this.systemEventSubscription = this.monitor.systemEvent$.subscribe(systemEvent => {
236
+ if (systemEvent.state === MotionMasterMessage.Status.SystemEvent.State.DEINITIALIZING) {
237
+ this.request.clearDevicesInformation();
238
+ }
239
+ });
240
+ }
241
+
242
+ stopMonitoring(messageId: string): void {
243
+ this.request.stopMonitoringDeviceParameterValues({ startMonitoringRequestId: messageId });
244
+ this.monitor.unsubscribe(messageId);
245
+ }
246
+
247
+ stopMonitoringAll(): void {
248
+ this.monitor.messageIds.forEach(this.stopMonitoring);
249
+ }
250
+
251
+ closeSockets() {
252
+ this.systemEventSubscription?.unsubscribe();
253
+ this.request.clearDevicesInformation();
254
+
255
+ this.reqResSocket.close();
256
+ this.pubSubSocket.close();
257
+ }
258
+
259
+ reopenSockets() {
260
+ this.reqResSocket.reopen();
261
+ this.pubSubSocket.reopen();
262
+
263
+ this.subscribeToSystemEvent();
264
+ }
265
+
266
+ }
@@ -0,0 +1,100 @@
1
+ import { bufferCount, distinctUntilChanged, filter, map, Observable, Subject, Subscription } from "rxjs";
2
+ import { v4 } from "uuid";
3
+ import { MonitoringConfig } from "./monitoring-config";
4
+ import { MotionMasterPubSubSocket } from "./motion-master-pub-sub-socket";
5
+ import { selectMotionMasterMessageByTopic, selectMotionMasterMessageStatusByKey } from "./operators";
6
+ import { IMotionMasterMessage, MotionMasterMessage } from "./types";
7
+ import * as equal from "fast-deep-equal";
8
+ import { MotionMasterPubSubWorkerSocket } from "./motion-master-pub-sub-worker-socket";
9
+
10
+ export class MotionMasterPubSubClient {
11
+
12
+ readonly data$ = new Subject<[string, IMotionMasterMessage[]]>();
13
+
14
+ private subscriptions = new Map<string, Subscription>();
15
+
16
+ readonly notification$ = this.socket.data$.pipe(
17
+ selectMotionMasterMessageByTopic('notification'),
18
+ );
19
+
20
+ readonly systemEvent$ = this.notification$.pipe(
21
+ selectMotionMasterMessageStatusByKey<MotionMasterMessage.Status.ISystemEvent>('systemEvent'),
22
+ );
23
+
24
+ readonly deviceEvent$ = this.notification$.pipe(
25
+ selectMotionMasterMessageStatusByKey<MotionMasterMessage.Status.IDeviceEvent>('deviceEvent'),
26
+ );
27
+
28
+ constructor(
29
+ public readonly socket: MotionMasterPubSubSocket,
30
+ ) { }
31
+
32
+ get messageIds(): string[] {
33
+ return Array.from(this.subscriptions.keys());
34
+ }
35
+
36
+ subscribe(config: MonitoringConfig): string {
37
+ const messageId = config.messageId ?? v4();
38
+ if (!config.messageId) {
39
+ config = { ...config, messageId };
40
+ }
41
+
42
+ if (this.socket instanceof MotionMasterPubSubWorkerSocket) {
43
+ this.socket.worker.postMessage({ subscribe: config });
44
+ } else {
45
+ const { bufferSize = 1, distinct = false, topic } = config;
46
+
47
+ let observable: Observable<IMotionMasterMessage> = this.socket.data$.pipe(
48
+ filter((data) => data[0] === topic),
49
+ map((data) => data[1]),
50
+ );
51
+
52
+ if (distinct) {
53
+ observable = observable.pipe(
54
+ distinctUntilChanged((prev, curr) => {
55
+ if (prev?.status?.monitoringParameterValues?.deviceParameterValues?.parameterValues
56
+ && curr?.status?.monitoringParameterValues?.deviceParameterValues?.parameterValues) {
57
+ return equal(
58
+ prev.status.monitoringParameterValues.deviceParameterValues.parameterValues,
59
+ curr.status.monitoringParameterValues.deviceParameterValues.parameterValues,
60
+ )
61
+ }
62
+ return false;
63
+ }),
64
+ );
65
+ }
66
+
67
+ const subscription = observable.pipe(
68
+ bufferCount(bufferSize),
69
+ ).subscribe((data) => {
70
+ this.data$.next([topic, data]);
71
+ });
72
+
73
+ this.subscriptions.set(messageId, subscription);
74
+ }
75
+
76
+ return messageId;
77
+ }
78
+
79
+ unsubscribe(messageId: string): void {
80
+ if (this.socket instanceof MotionMasterPubSubWorkerSocket) {
81
+ this.socket.worker.postMessage({ unsubcribe: { messageId } });
82
+ } else {
83
+ const subscription = this.subscriptions.get(messageId);
84
+ if (subscription) {
85
+ subscription.unsubscribe();
86
+ this.subscriptions.delete(messageId);
87
+ }
88
+ }
89
+ }
90
+
91
+ unsubscribeAll(): void {
92
+ if (this.socket instanceof MotionMasterPubSubWorkerSocket) {
93
+ this.socket.worker.postMessage({ unsubscribeAll: true });
94
+ } else {
95
+ this.subscriptions.forEach((subscription) => subscription.unsubscribe());
96
+ this.subscriptions.clear();
97
+ }
98
+ }
99
+
100
+ }
@@ -0,0 +1,46 @@
1
+ import { BehaviorSubject, Observable } from "rxjs";
2
+ import { IMotionMasterMessage } from "./types";
3
+
4
+ export interface MotionMasterPubSubSocket {
5
+ /**
6
+ * Emits a boolean value when socket gets opened or closed.
7
+ */
8
+ readonly opened$: BehaviorSubject<boolean>;
9
+
10
+ /**
11
+ * Emits close events with code and reason data.
12
+ * @link https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close_event
13
+ */
14
+ readonly close$: Observable<{ code: number; reason: string; }>
15
+
16
+ /**
17
+ * Incoming data is a tuple of topic and decoded message.
18
+ */
19
+ readonly data$: Observable<[string, IMotionMasterMessage]>;
20
+
21
+ /**
22
+ * URL set in call to open.
23
+ */
24
+ get url(): string | undefined;
25
+
26
+ /**
27
+ * Open socket.
28
+ *
29
+ * The connected$ and eventually alive$ observables will emit true values.
30
+ */
31
+ open(url: string): void;
32
+
33
+ /**
34
+ * Reopen socket.
35
+ *
36
+ * Reopen if closed using the same url previously set in open.
37
+ */
38
+ reopen(): void;
39
+
40
+ /**
41
+ * Close socket.
42
+ *
43
+ * The opened$ and alive$ observables will emit false values.
44
+ */
45
+ close(): void;
46
+ }
@@ -0,0 +1,77 @@
1
+ import { BehaviorSubject, Subject } from "rxjs";
2
+ import { webSocket, WebSocketSubject, WebSocketSubjectConfig } from "rxjs/webSocket";
3
+ import { logger } from "./logger";
4
+ import { MotionMasterPubSubSocket } from "./motion-master-pub-sub-socket";
5
+ import { IMotionMasterMessage, MotionMasterMessage } from "./types";
6
+ import { createPlainObjectFromMotionMasterMessage } from "./util";
7
+
8
+ export class MotionMasterPubSubWebSocket implements MotionMasterPubSubSocket {
9
+
10
+ readonly opened$ = new BehaviorSubject<boolean>(false);
11
+
12
+ readonly close$ = new Subject<{ code: number; reason: string; }>();
13
+
14
+ readonly data$ = new Subject<[string, IMotionMasterMessage]>();
15
+
16
+ private webSocket$?: WebSocketSubject<[string, IMotionMasterMessage]>;
17
+
18
+ private _url?: string;
19
+
20
+ get url(): string | undefined {
21
+ return this._url;
22
+ }
23
+
24
+ open(url: string): void {
25
+ this._url = url;
26
+
27
+ this.webSocket$ = this.createWebSocket(url);
28
+
29
+ this.webSocket$.subscribe({
30
+ next: (value) => this.data$.next(value),
31
+ error: () => {
32
+ // ignore, clients will receive close$ event instead
33
+ },
34
+ });
35
+ }
36
+
37
+ close(): void {
38
+ this.webSocket$?.complete();
39
+ }
40
+
41
+ reopen() {
42
+ if (this.url && this.opened$.value === false) {
43
+ this.open(this.url);
44
+ }
45
+ }
46
+
47
+ private createWebSocket(url: string) {
48
+ const decoder = new TextDecoder('utf-8');
49
+
50
+ const webSocketConfig: WebSocketSubjectConfig<[string, IMotionMasterMessage]> = {
51
+ binaryType: 'arraybuffer',
52
+ closeObserver: {
53
+ next: (ev: CloseEvent) => {
54
+ this.opened$.next(false);
55
+ this.close$.next({ code: ev.code, reason: ev.reason });
56
+ logger.info(`Connection closed ${url}`);
57
+ },
58
+ },
59
+ deserializer: (e: MessageEvent<ArrayBuffer>): [string, IMotionMasterMessage] => {
60
+ const end = new Uint8Array(e.data, 0, 1)[0] + 1;
61
+ const topic = decoder.decode(e.data.slice(1, end)); // topic starts from the 2nd byte
62
+ const message = MotionMasterMessage.decode(new Uint8Array(e.data.slice(end)));
63
+ return [topic, createPlainObjectFromMotionMasterMessage(message)];
64
+ },
65
+ openObserver: {
66
+ next: () => {
67
+ this.opened$.next(true);
68
+ logger.info(`Connection opened ${url}`);
69
+ },
70
+ },
71
+ url,
72
+ };
73
+
74
+ return webSocket(webSocketConfig);
75
+ }
76
+
77
+ }