abb-rws-client 0.7.2 → 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 (70) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +17 -8
  3. package/dist/HalJsonParser.d.ts +55 -0
  4. package/dist/HalJsonParser.d.ts.map +1 -0
  5. package/dist/HalJsonParser.js +155 -0
  6. package/dist/HalJsonParser.js.map +1 -0
  7. package/dist/HttpSession.d.ts.map +1 -1
  8. package/dist/HttpSession.js +13 -2
  9. package/dist/HttpSession.js.map +1 -1
  10. package/dist/IRWSAdapter.d.ts +7 -1
  11. package/dist/IRWSAdapter.d.ts.map +1 -1
  12. package/dist/MdnsDiscovery.d.ts +57 -0
  13. package/dist/MdnsDiscovery.d.ts.map +1 -0
  14. package/dist/MdnsDiscovery.js +313 -0
  15. package/dist/MdnsDiscovery.js.map +1 -0
  16. package/dist/MultiRobotManager.d.ts +5 -2
  17. package/dist/MultiRobotManager.d.ts.map +1 -1
  18. package/dist/MultiRobotManager.js +8 -3
  19. package/dist/MultiRobotManager.js.map +1 -1
  20. package/dist/RWS1Adapter.d.ts +60 -2
  21. package/dist/RWS1Adapter.d.ts.map +1 -1
  22. package/dist/RWS1Adapter.js +152 -4
  23. package/dist/RWS1Adapter.js.map +1 -1
  24. package/dist/ResourceMapper.d.ts +4 -0
  25. package/dist/ResourceMapper.d.ts.map +1 -1
  26. package/dist/ResourceMapper.js +9 -1
  27. package/dist/ResourceMapper.js.map +1 -1
  28. package/dist/RobotManager.d.ts +72 -12
  29. package/dist/RobotManager.d.ts.map +1 -1
  30. package/dist/RobotManager.js +255 -50
  31. package/dist/RobotManager.js.map +1 -1
  32. package/dist/RwsClient2.d.ts +150 -10
  33. package/dist/RwsClient2.d.ts.map +1 -1
  34. package/dist/RwsClient2.js +599 -236
  35. package/dist/RwsClient2.js.map +1 -1
  36. package/dist/WsSubscriber.d.ts +31 -5
  37. package/dist/WsSubscriber.d.ts.map +1 -1
  38. package/dist/WsSubscriber.js +104 -50
  39. package/dist/WsSubscriber.js.map +1 -1
  40. package/dist/detect.d.ts +12 -3
  41. package/dist/detect.d.ts.map +1 -1
  42. package/dist/detect.js +69 -25
  43. package/dist/detect.js.map +1 -1
  44. package/dist/index.d.ts +3 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +2 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/types.js +3 -3
  49. package/dist/types.js.map +1 -1
  50. package/examples/05-remote-control-rmmp.mjs +10 -12
  51. package/examples/06-pull-module-source.mjs +13 -20
  52. package/package.json +62 -60
  53. package/src/HalJsonParser.ts +137 -0
  54. package/src/HttpSession.ts +460 -0
  55. package/src/IRWSAdapter.ts +422 -0
  56. package/src/Logger.ts +54 -0
  57. package/src/MdnsDiscovery.ts +336 -0
  58. package/src/MultiRobotManager.ts +159 -0
  59. package/src/RWS1Adapter.ts +1018 -0
  60. package/src/RWS2Adapter.ts +19 -0
  61. package/src/ResourceMapper.ts +517 -0
  62. package/src/ResponseParser.ts +710 -0
  63. package/src/RobotManager.ts +1705 -0
  64. package/src/RwsClient.ts +1150 -0
  65. package/src/RwsClient2.ts +2214 -0
  66. package/src/WsSubscriber.ts +350 -0
  67. package/src/XhtmlParser.ts +53 -0
  68. package/src/detect.ts +261 -0
  69. package/src/index.ts +83 -0
  70. package/src/types.ts +336 -0
@@ -0,0 +1,1150 @@
1
+ /**
2
+ * RwsClient — the single public class for the abb-rws-client package.
3
+ *
4
+ * Assembles HttpSession, ResourceMapper, ResponseParser, and WsSubscriber into
5
+ * a convenient typed API for ABB IRC5 robot controllers using RWS 1.0.
6
+ *
7
+ * Compatible with RobotWare 6.x only. NOT compatible with RWS 2.0 / RobotWare 7.x / OmniCore.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const client = new RwsClient({ host: '127.0.0.1' });
12
+ * await client.connect();
13
+ * const state = await client.getControllerState();
14
+ * await client.disconnect();
15
+ * ```
16
+ */
17
+
18
+ import { HttpSession } from './HttpSession.js';
19
+ import type { HttpSessionOptions } from './HttpSession.js';
20
+ import { WsSubscriber } from './WsSubscriber.js';
21
+ import { RwsError } from './types.js';
22
+ import type {
23
+ RwsClientOptions,
24
+ ControllerState,
25
+ OperationMode,
26
+ ExecutionState,
27
+ ExecutionInfo,
28
+ ExecutionCycle,
29
+ JointTarget,
30
+ RobTarget,
31
+ CartesianFull,
32
+ Signal,
33
+ RapidTask,
34
+ IoNetwork,
35
+ IoDevice,
36
+ SystemInfo,
37
+ ControllerIdentity,
38
+ ElogMessage,
39
+ FileEntry,
40
+ MastershipDomain,
41
+ CollisionDetectionState,
42
+ RapidSymbolProperties,
43
+ ControllerClock,
44
+ UiInstruction,
45
+ RapidSymbolInfo,
46
+ RapidSymbolSearchParams,
47
+ RestartMode,
48
+ SubscriptionResource,
49
+ SubscriptionEvent,
50
+ } from './types.js';
51
+ import {
52
+ controllerState as pathControllerState,
53
+ setControllerState as mapSetControllerState,
54
+ operationMode as pathOperationMode,
55
+ speedRatio as pathSpeedRatio,
56
+ setSpeedRatio as mapSetSpeedRatio,
57
+ collisionDetectionState as pathCollisionDetectionState,
58
+ restartController as mapRestartController,
59
+ lockOperationMode as mapLockOperationMode,
60
+ unlockOperationMode as mapUnlockOperationMode,
61
+ rapidTasks as pathRapidTasks,
62
+ rapidExecutionState as pathRapidExecutionState,
63
+ startRapid as mapStartRapid,
64
+ stopRapid as mapStopRapid,
65
+ resetRapid as mapResetRapid,
66
+ setExecutionCycle as mapSetExecutionCycle,
67
+ rapidSymbol as pathRapidSymbol,
68
+ rapidSymbolProperties as pathRapidSymbolProperties,
69
+ setRapidSymbol as mapSetRapidSymbol,
70
+ activeUiInstruction as pathActiveUiInstruction,
71
+ setUiInstructionParam as mapSetUiInstructionParam,
72
+ activateRapidTask as mapActivateRapidTask,
73
+ deactivateRapidTask as mapDeactivateRapidTask,
74
+ activateAllRapidTasks as mapActivateAllRapidTasks,
75
+ deactivateAllRapidTasks as mapDeactivateAllRapidTasks,
76
+ searchRapidSymbols as mapSearchRapidSymbols,
77
+ validateRapidValue as mapValidateRapidValue,
78
+ jointTarget as pathJointTarget,
79
+ robTarget as pathRobTarget,
80
+ cartesianFull as pathCartesianFull,
81
+ loadModule as mapLoadModule,
82
+ listModules as pathListModules,
83
+ uploadFile as pathUploadFile,
84
+ allSignals as pathAllSignals,
85
+ networks as pathNetworks,
86
+ devices as pathDevices,
87
+ signal as pathSignal,
88
+ setSignal as mapSetSignal,
89
+ systemInfo as pathSystemInfo,
90
+ controllerIdentity as pathControllerIdentity,
91
+ clockInfo as pathClockInfo,
92
+ setControllerClock as mapSetControllerClock,
93
+ elogMessages as pathElogMessages,
94
+ clearElogDomain as mapClearElogDomain,
95
+ clearAllElogs as mapClearAllElogs,
96
+ fileServicePath,
97
+ deleteFile as pathDeleteFile,
98
+ copyFile as mapCopyFile,
99
+ requestMastership as mapRequestMastership,
100
+ releaseMastership as mapReleaseMastership,
101
+ } from './ResourceMapper.js';
102
+ import {
103
+ parseControllerState,
104
+ parseOperationMode,
105
+ parseSpeedRatio,
106
+ parseExecutionState,
107
+ parseExecutionInfo,
108
+ parseJointTarget,
109
+ parseRobTarget,
110
+ parseCartesianFull,
111
+ parseRapidSymbolValue,
112
+ parseRapidSymbolProperties,
113
+ parseSignal,
114
+ parseSignalList,
115
+ parseNetworks,
116
+ parseDevices,
117
+ parseRapidTasks,
118
+ parseSystemInfo,
119
+ parseControllerIdentity,
120
+ parseControllerClock,
121
+ parseActiveUiInstruction,
122
+ parseRapidSymbolSearch,
123
+ parseElogMessages,
124
+ parseDirectory,
125
+ parseCollisionDetectionState,
126
+ } from './ResponseParser.js';
127
+
128
+ export class RwsClient {
129
+ private readonly session: HttpSession;
130
+ private readonly subscriber: WsSubscriber;
131
+
132
+ constructor(options: RwsClientOptions) {
133
+ const sessionOptions: HttpSessionOptions = {
134
+ baseUrl: `http://${options.host}:${options.port ?? 80}`,
135
+ username: options.username ?? 'Admin',
136
+ password: options.password ?? 'robotics',
137
+ requestIntervalMs: options.requestIntervalMs ?? 55,
138
+ timeoutMs: options.timeout ?? 5000,
139
+ sessionCookie: options.sessionCookie,
140
+ };
141
+ this.session = new HttpSession(sessionOptions);
142
+ this.subscriber = new WsSubscriber(this.session, options.host, options.port ?? 80);
143
+ }
144
+
145
+ /** Returns the current -http-session- cookie so callers can persist and reuse it */
146
+ getSessionCookie(): string | null {
147
+ return this.session.getSessionCookie();
148
+ }
149
+
150
+ /**
151
+ * Generic HTTP request helper exposing the underlying authenticated session.
152
+ *
153
+ * Lets adapters call any RWS endpoint without requiring this library to wrap
154
+ * each one in a typed method. Useful for endpoints that aren't (yet) exposed
155
+ * as named methods — the caller handles parsing the response body.
156
+ *
157
+ * Reuses the session's digest auth, cookie, queue, retry, and timeout logic.
158
+ *
159
+ * @param method 'GET' | 'POST' | 'PUT' | 'DELETE'
160
+ * @param path URL path, e.g. `/rw/cfg/MOC/ROBOT/instances?json=1`
161
+ * @param body Optional request body (form-encoded string for POST/PUT)
162
+ * @returns `{ status: number, body: string }` — raw response
163
+ */
164
+ async request(
165
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE',
166
+ path: string,
167
+ body?: string,
168
+ ): Promise<{ status: number; body: string }> {
169
+ let res;
170
+ if (method === 'GET') { res = await this.session.get(path); }
171
+ else if (method === 'POST') { res = await this.session.post(path, body ?? ''); }
172
+ else if (method === 'PUT') { res = await this.session.put(path, body ?? ''); }
173
+ else { res = await this.session.delete(path); }
174
+ return { status: res.status, body: res.body };
175
+ }
176
+
177
+ // ─── Connection ─────────────────────────────────────────────────────────────
178
+
179
+ /**
180
+ * Establish a session with the controller.
181
+ * Triggers digest authentication and verifies connectivity by reading controller state.
182
+ * Must be called before any other method.
183
+ *
184
+ * @throws {RwsError} code='NETWORK_ERROR' if the controller is unreachable
185
+ * @throws {RwsError} code='AUTH_FAILED' if credentials are incorrect
186
+ */
187
+ async connect(): Promise<void> {
188
+ try {
189
+ const { body } = await this.session.get(pathControllerState());
190
+ parseControllerState(body); // validate response is parseable
191
+ } catch (e) {
192
+ if (e instanceof RwsError) throw e;
193
+ throw new RwsError(`Failed to connect: ${String(e)}`, 'NETWORK_ERROR');
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Disconnect from the controller.
199
+ * Closes all WebSocket subscriptions and clears the session.
200
+ */
201
+ async disconnect(): Promise<void> {
202
+ try {
203
+ await this.subscriber.closeAll();
204
+ } catch { /* ignore */ }
205
+ // Server-side cleanup: GET /logout releases mastership, subscriptions, and frees
206
+ // the session slot in the controller's pool. Without this, orphan mastership can
207
+ // block subsequent clients for several minutes (until the controller times out
208
+ // the session by inactivity). Best-effort — ignore errors.
209
+ try { await this.session.get('/logout'); } catch { /* ignore */ }
210
+ this.session.clearSession();
211
+ }
212
+
213
+ // ─── Controller state ───────────────────────────────────────────────────────
214
+
215
+ /**
216
+ * Read the current controller state.
217
+ *
218
+ * @returns 'motoron' | 'motoroff' | 'init' | 'guardstop' | 'emergencystop' |
219
+ * 'emergencystopreset' | 'sysfail'
220
+ * @throws {RwsError} code='PARSE_ERROR' on unexpected response format
221
+ */
222
+ async getControllerState(): Promise<ControllerState> {
223
+ try {
224
+ const { body } = await this.session.get(pathControllerState());
225
+ return parseControllerState(body);
226
+ } catch (e) {
227
+ if (e instanceof RwsError) throw e;
228
+ throw new RwsError(`getControllerState failed: ${String(e)}`, 'UNKNOWN');
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Set the controller motor state.
234
+ * Requires AUTO mode and mastership (request with requestMastership('motion') first).
235
+ *
236
+ * @param state - 'motoron' | 'motoroff'
237
+ * @throws {RwsError} code='AUTH_FAILED' if mastership is not held
238
+ */
239
+ async setControllerState(state: 'motoron' | 'motoroff'): Promise<void> {
240
+ try {
241
+ const { path, body } = mapSetControllerState(state);
242
+ await this.session.post(path, body);
243
+ } catch (e) {
244
+ if (e instanceof RwsError) throw e;
245
+ throw new RwsError(`setControllerState failed: ${String(e)}`, 'UNKNOWN');
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Read the current operation mode.
251
+ *
252
+ * @returns 'AUTO' | 'MANR' | 'MANF'
253
+ * @throws {RwsError} code='PARSE_ERROR' on unexpected response format
254
+ */
255
+ async getOperationMode(): Promise<OperationMode> {
256
+ try {
257
+ const { body } = await this.session.get(pathOperationMode());
258
+ return parseOperationMode(body);
259
+ } catch (e) {
260
+ if (e instanceof RwsError) throw e;
261
+ throw new RwsError(`getOperationMode failed: ${String(e)}`, 'UNKNOWN');
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Read the collision detection state.
267
+ * Returns INIT (no collision), TRIGGERED, CONFIRMED, or TRIGGERED_ACK.
268
+ * Requires the Collision Detection option on the controller.
269
+ */
270
+ async getCollisionDetectionState(): Promise<CollisionDetectionState> {
271
+ try {
272
+ const { body } = await this.session.get(pathCollisionDetectionState());
273
+ return parseCollisionDetectionState(body);
274
+ } catch (e) {
275
+ if (e instanceof RwsError) throw e;
276
+ throw new RwsError(`getCollisionDetectionState failed: ${String(e)}`, 'UNKNOWN');
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Restart (or warm-start) the controller.
282
+ *
283
+ * **Modes:**
284
+ * - `restart` — Normal restart; saves state and activates changed system parameters
285
+ * - `istart` — Restart with original installation settings; discards all programs
286
+ * - `pstart` — Restart preserving system parameters; removes programs
287
+ * - `bstart` — Boot with last auto-saved state (crash recovery)
288
+ *
289
+ * @param mode - Restart mode; default 'restart'
290
+ */
291
+ async restartController(mode: RestartMode = 'restart'): Promise<void> {
292
+ try {
293
+ const { path, body } = mapRestartController(mode);
294
+ await this.session.post(path, body);
295
+ } catch (e) {
296
+ if (e instanceof RwsError) throw e;
297
+ throw new RwsError(`restartController failed: ${String(e)}`, 'UNKNOWN');
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Lock the operation mode selector on the FlexPendant.
303
+ * @param pin - 4-digit PIN code
304
+ * @param permanent - true = permanent lock; false = temporary
305
+ */
306
+ async lockOperationMode(pin: string, permanent = false): Promise<void> {
307
+ try {
308
+ const { path, body } = mapLockOperationMode(pin, permanent);
309
+ await this.session.post(path, body);
310
+ } catch (e) {
311
+ if (e instanceof RwsError) throw e;
312
+ throw new RwsError(`lockOperationMode failed: ${String(e)}`, 'UNKNOWN');
313
+ }
314
+ }
315
+
316
+ /** Unlock the operation mode selector. */
317
+ async unlockOperationMode(): Promise<void> {
318
+ try {
319
+ const { path, body } = mapUnlockOperationMode();
320
+ await this.session.post(path, body);
321
+ } catch (e) {
322
+ if (e instanceof RwsError) throw e;
323
+ throw new RwsError(`unlockOperationMode failed: ${String(e)}`, 'UNKNOWN');
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Switch the controller's operation mode. **Virtual controllers only** — on
329
+ * real IRC5 hardware the physical key switch on the FlexPendant is the
330
+ * source of truth and the controller will reject this with 403.
331
+ *
332
+ * RWS 1.0 endpoint: POST /rw/panel/opmode with body `opmode=auto|man|manfs`
333
+ * (note the lowercase pre-OmniCore values).
334
+ */
335
+ async setOperationMode(mode: 'AUTO' | 'MANR' | 'MANF'): Promise<void> {
336
+ const wire = mode === 'AUTO' ? 'auto' : mode === 'MANR' ? 'man' : 'manfs';
337
+ try {
338
+ await this.session.post('/rw/panel/opmode', `opmode=${wire}`);
339
+ } catch (e) {
340
+ if (e instanceof RwsError) { throw e; }
341
+ throw new RwsError(`setOperationMode failed: ${String(e)}`, 'UNKNOWN');
342
+ }
343
+ }
344
+
345
+ // ─── Speed ratio ────────────────────────────────────────────────────────────
346
+
347
+ /**
348
+ * Read the current speed ratio (0–100).
349
+ * Represents override percentage applied to all robot speeds.
350
+ */
351
+ async getSpeedRatio(): Promise<number> {
352
+ try {
353
+ const { body } = await this.session.get(pathSpeedRatio());
354
+ return parseSpeedRatio(body);
355
+ } catch (e) {
356
+ if (e instanceof RwsError) throw e;
357
+ throw new RwsError(`getSpeedRatio failed: ${String(e)}`, 'UNKNOWN');
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Set the speed ratio override (0–100). Only valid in AUTO mode.
363
+ * @param ratio - Integer 0–100 (clamped automatically)
364
+ */
365
+ async setSpeedRatio(ratio: number): Promise<void> {
366
+ try {
367
+ const { path, body } = mapSetSpeedRatio(ratio);
368
+ await this.session.post(path, body);
369
+ } catch (e) {
370
+ if (e instanceof RwsError) throw e;
371
+ throw new RwsError(`setSpeedRatio failed: ${String(e)}`, 'UNKNOWN');
372
+ }
373
+ }
374
+
375
+ // ─── RAPID execution ────────────────────────────────────────────────────────
376
+
377
+ /**
378
+ * Read the current RAPID execution state.
379
+ *
380
+ * @returns 'running' | 'stopped'
381
+ */
382
+ async getRapidExecutionState(): Promise<ExecutionState> {
383
+ try {
384
+ const { body } = await this.session.get(pathRapidExecutionState());
385
+ return parseExecutionState(body);
386
+ } catch (e) {
387
+ if (e instanceof RwsError) throw e;
388
+ throw new RwsError(`getRapidExecutionState failed: ${String(e)}`, 'UNKNOWN');
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Read the full RAPID execution info including state and current cycle mode.
394
+ *
395
+ * @returns ExecutionInfo with state ('running'|'stopped') and cycle ('once'|'forever'|'asis'|'oncedone')
396
+ */
397
+ async getRapidExecutionInfo(): Promise<ExecutionInfo> {
398
+ try {
399
+ const { body } = await this.session.get(pathRapidExecutionState());
400
+ return parseExecutionInfo(body);
401
+ } catch (e) {
402
+ if (e instanceof RwsError) throw e;
403
+ throw new RwsError(`getRapidExecutionInfo failed: ${String(e)}`, 'UNKNOWN');
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Retrieve all RAPID tasks and their current states.
409
+ *
410
+ * @returns Array of RapidTask objects
411
+ */
412
+ async getRapidTasks(): Promise<RapidTask[]> {
413
+ try {
414
+ const { body } = await this.session.get(pathRapidTasks());
415
+ return parseRapidTasks(body);
416
+ } catch (e) {
417
+ if (e instanceof RwsError) throw e;
418
+ throw new RwsError(`getRapidTasks failed: ${String(e)}`, 'UNKNOWN');
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Start RAPID program execution.
424
+ * The controller must be in AUTO mode with motors on.
425
+ *
426
+ * @throws {RwsError} code='MOTORS_OFF' if motors are not on
427
+ * @throws {RwsError} code='CONTROLLER_BUSY' if the controller is already busy
428
+ */
429
+ async startRapid(): Promise<void> {
430
+ try {
431
+ const { path, body } = mapStartRapid();
432
+ await this.session.post(path, body);
433
+ } catch (e) {
434
+ if (e instanceof RwsError) {
435
+ // Map 400 "motors off" to a more descriptive code
436
+ if (e.httpStatus === 400 && e.rwsDetail?.includes('motor')) {
437
+ throw new RwsError('Motors are off — enable motors before starting RAPID', 'MOTORS_OFF', e.httpStatus, e.rwsDetail);
438
+ }
439
+ throw e;
440
+ }
441
+ throw new RwsError(`startRapid failed: ${String(e)}`, 'UNKNOWN');
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Stop RAPID program execution.
447
+ */
448
+ async stopRapid(): Promise<void> {
449
+ try {
450
+ const { path, body } = mapStopRapid();
451
+ await this.session.post(path, body);
452
+ } catch (e) {
453
+ if (e instanceof RwsError) throw e;
454
+ throw new RwsError(`stopRapid failed: ${String(e)}`, 'UNKNOWN');
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Reset the RAPID program pointer to main.
460
+ * RAPID must be stopped before calling this.
461
+ */
462
+ async resetRapid(): Promise<void> {
463
+ try {
464
+ const { path, body } = mapResetRapid();
465
+ await this.session.post(path, body);
466
+ } catch (e) {
467
+ if (e instanceof RwsError) throw e;
468
+ throw new RwsError(`resetRapid failed: ${String(e)}`, 'UNKNOWN');
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Set the RAPID execution cycle mode.
474
+ * @param cycle - 'once' (run once then stop) | 'forever' (loop) | 'asis' (keep current)
475
+ */
476
+ async setExecutionCycle(cycle: ExecutionCycle): Promise<void> {
477
+ try {
478
+ const { path, body } = mapSetExecutionCycle(cycle);
479
+ await this.session.post(path, body);
480
+ } catch (e) {
481
+ if (e instanceof RwsError) throw e;
482
+ throw new RwsError(`setExecutionCycle failed: ${String(e)}`, 'UNKNOWN');
483
+ }
484
+ }
485
+
486
+ // ─── RAPID variables ────────────────────────────────────────────────────────
487
+
488
+ /**
489
+ * Read the value of a RAPID symbol (variable, persistent, or constant).
490
+ * Returns the raw string as the controller formats it (e.g. '42', '"hello"', '[1,2,3]').
491
+ *
492
+ * @param taskName - RAPID task name, e.g. 'T_ROB1'
493
+ * @param moduleName - Module name, e.g. 'user'
494
+ * @param symbolName - Symbol name, e.g. 'reg1'
495
+ */
496
+ async getRapidVariable(taskName: string, moduleName: string, symbolName: string): Promise<string> {
497
+ try {
498
+ const { body } = await this.session.get(pathRapidSymbol(taskName, moduleName, symbolName));
499
+ return parseRapidSymbolValue(body);
500
+ } catch (e) {
501
+ if (e instanceof RwsError) throw e;
502
+ throw new RwsError(`getRapidVariable failed: ${String(e)}`, 'UNKNOWN');
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Write a value to a RAPID variable or persistent.
508
+ * Value must be a RAPID-formatted string: e.g. '42', '3.14', '"hello"', '[1,0,0,0]'.
509
+ *
510
+ * @param taskName - RAPID task name
511
+ * @param moduleName - Module name
512
+ * @param symbolName - Symbol name
513
+ * @param value - New value in RAPID syntax
514
+ */
515
+ async setRapidVariable(taskName: string, moduleName: string, symbolName: string, value: string): Promise<void> {
516
+ try {
517
+ const { path, body } = mapSetRapidSymbol(taskName, moduleName, symbolName, value);
518
+ await this.session.post(path, body);
519
+ } catch (e) {
520
+ if (e instanceof RwsError) throw e;
521
+ throw new RwsError(`setRapidVariable failed: ${String(e)}`, 'UNKNOWN');
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Activate a RAPID task (for multitasking systems).
527
+ * Mastership is taken internally by the controller.
528
+ * @param task - Task name, e.g. 'T_ROB2'
529
+ */
530
+ async activateRapidTask(task: string): Promise<void> {
531
+ try {
532
+ const { path, body } = mapActivateRapidTask(task);
533
+ await this.session.post(path, body);
534
+ } catch (e) {
535
+ if (e instanceof RwsError) throw e;
536
+ throw new RwsError(`activateRapidTask failed: ${String(e)}`, 'UNKNOWN');
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Deactivate a RAPID task (for multitasking systems).
542
+ * @param task - Task name
543
+ */
544
+ async deactivateRapidTask(task: string): Promise<void> {
545
+ try {
546
+ const { path, body } = mapDeactivateRapidTask(task);
547
+ await this.session.post(path, body);
548
+ } catch (e) {
549
+ if (e instanceof RwsError) throw e;
550
+ throw new RwsError(`deactivateRapidTask failed: ${String(e)}`, 'UNKNOWN');
551
+ }
552
+ }
553
+
554
+ /** Activate ALL RAPID tasks. */
555
+ async activateAllRapidTasks(): Promise<void> {
556
+ try {
557
+ const { path, body } = mapActivateAllRapidTasks();
558
+ await this.session.post(path, body);
559
+ } catch (e) {
560
+ if (e instanceof RwsError) throw e;
561
+ throw new RwsError(`activateAllRapidTasks failed: ${String(e)}`, 'UNKNOWN');
562
+ }
563
+ }
564
+
565
+ /** Deactivate ALL RAPID tasks. */
566
+ async deactivateAllRapidTasks(): Promise<void> {
567
+ try {
568
+ const { path, body } = mapDeactivateAllRapidTasks();
569
+ await this.session.post(path, body);
570
+ } catch (e) {
571
+ if (e instanceof RwsError) throw e;
572
+ throw new RwsError(`deactivateAllRapidTasks failed: ${String(e)}`, 'UNKNOWN');
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Get the currently active RAPID UI instruction.
578
+ * Returns null if no UI instruction is waiting for input.
579
+ * Used to detect when RAPID is waiting for operator input (TPReadNum, TPReadFK, etc.).
580
+ */
581
+ async getActiveUiInstruction(): Promise<UiInstruction | null> {
582
+ try {
583
+ const { body } = await this.session.get(pathActiveUiInstruction());
584
+ return parseActiveUiInstruction(body);
585
+ } catch (e) {
586
+ if (e instanceof RwsError) throw e;
587
+ throw new RwsError(`getActiveUiInstruction failed: ${String(e)}`, 'UNKNOWN');
588
+ }
589
+ }
590
+
591
+ /**
592
+ * Respond to an active RAPID UI instruction (e.g. send the answer to a TPReadNum).
593
+ * Get the stackurl from getActiveUiInstruction().stack.
594
+ *
595
+ * Common parameter names:
596
+ * - 'Result' — the answer value for TPReadNum, TPReadFK
597
+ * - 'TPFK1' … 'TPFK5' — individual function key states (0/1)
598
+ * - 'TPCompleted' — set to 'TRUE' when done
599
+ *
600
+ * @param stackurl - Stack URL from UiInstruction.stack (e.g. 'RAPID/T_ROB1/%$104')
601
+ * @param uiparam - Parameter name, e.g. 'Result'
602
+ * @param value - Value to set, e.g. '42' or 'TRUE'
603
+ */
604
+ async setUiInstructionParam(stackurl: string, uiparam: string, value: string): Promise<void> {
605
+ try {
606
+ const { path, body } = mapSetUiInstructionParam(stackurl, uiparam, value);
607
+ await this.session.post(path, body);
608
+ } catch (e) {
609
+ if (e instanceof RwsError) throw e;
610
+ throw new RwsError(`setUiInstructionParam failed: ${String(e)}`, 'UNKNOWN');
611
+ }
612
+ }
613
+
614
+ /**
615
+ * Search for RAPID symbols matching filter criteria.
616
+ *
617
+ * @param params - Search parameters (task is required; all others are optional filters)
618
+ * @returns Array of matching symbols with abbreviated properties
619
+ *
620
+ * @example
621
+ * ```ts
622
+ * // Find all persistent variables in T_ROB1
623
+ * const persistents = await client.searchRapidSymbols({ task: 'T_ROB1', symtyp: 'per' });
624
+ * ```
625
+ */
626
+ async searchRapidSymbols(params: RapidSymbolSearchParams): Promise<RapidSymbolInfo[]> {
627
+ try {
628
+ const { path, body } = mapSearchRapidSymbols(params);
629
+ const { body: responseBody } = await this.session.post(path, body);
630
+ return parseRapidSymbolSearch(responseBody);
631
+ } catch (e) {
632
+ if (e instanceof RwsError) throw e;
633
+ throw new RwsError(`searchRapidSymbols failed: ${String(e)}`, 'UNKNOWN');
634
+ }
635
+ }
636
+
637
+ /**
638
+ * Validate a value against a RAPID data type without writing it.
639
+ * Useful for validating user input before calling setRapidVariable.
640
+ *
641
+ * @param task - RAPID task name, e.g. 'T_ROB1'
642
+ * @param value - Value in RAPID syntax, e.g. '[1,0,0,0]'
643
+ * @param datatype - RAPID data type name, e.g. 'tooldata', 'robtarget', 'num'
644
+ * @returns true if valid, false if invalid (no exception thrown for invalid values)
645
+ */
646
+ async validateRapidValue(task: string, value: string, datatype: string): Promise<boolean> {
647
+ try {
648
+ const { path, body } = mapValidateRapidValue(task, value, datatype);
649
+ const { status } = await this.session.post(path, body);
650
+ return status === 204;
651
+ } catch (e) {
652
+ if (e instanceof RwsError && e.httpStatus === 400) return false;
653
+ if (e instanceof RwsError) throw e;
654
+ throw new RwsError(`validateRapidValue failed: ${String(e)}`, 'UNKNOWN');
655
+ }
656
+ }
657
+
658
+ /**
659
+ * Read RAPID symbol properties (type, dimensions, storage class, etc.).
660
+ * Useful for introspecting variables, persistents, constants, and records.
661
+ *
662
+ * @param taskName - RAPID task name, e.g. 'T_ROB1'
663
+ * @param moduleName - Module name, e.g. 'user'
664
+ * @param symbolName - Symbol name, e.g. 'reg1'
665
+ */
666
+ async getRapidSymbolProperties(taskName: string, moduleName: string, symbolName: string): Promise<RapidSymbolProperties> {
667
+ try {
668
+ const { body } = await this.session.get(pathRapidSymbolProperties(taskName, moduleName, symbolName));
669
+ return parseRapidSymbolProperties(body);
670
+ } catch (e) {
671
+ if (e instanceof RwsError) throw e;
672
+ throw new RwsError(`getRapidSymbolProperties failed: ${String(e)}`, 'UNKNOWN');
673
+ }
674
+ }
675
+
676
+ // ─── Motion ─────────────────────────────────────────────────────────────────
677
+
678
+ /**
679
+ * Read the current joint-space positions for a mechanical unit.
680
+ *
681
+ * @param mechunit - Mechanical unit name; default 'ROB_1'
682
+ * @returns JointTarget with rax_1 … rax_6 in degrees
683
+ */
684
+ async getJointPositions(mechunit?: string): Promise<JointTarget> {
685
+ try {
686
+ const { body } = await this.session.get(pathJointTarget(mechunit));
687
+ return parseJointTarget(body);
688
+ } catch (e) {
689
+ if (e instanceof RwsError) throw e;
690
+ throw new RwsError(`getJointPositions failed: ${String(e)}`, 'UNKNOWN');
691
+ }
692
+ }
693
+
694
+ /**
695
+ * Read the current Cartesian robot target (TCP position and orientation).
696
+ *
697
+ * @param mechunit - Mechanical unit; default 'ROB_1'
698
+ * @param tool - Active tool frame; default 'tool0'
699
+ * @param wobj - Active work object; default 'wobj0'
700
+ * @returns RobTarget with x, y, z (mm) and q1–q4 quaternion components
701
+ */
702
+ async getCartesianPosition(mechunit?: string, tool?: string, wobj?: string): Promise<RobTarget> {
703
+ try {
704
+ const { body } = await this.session.get(pathRobTarget(mechunit, tool, wobj));
705
+ return parseRobTarget(body);
706
+ } catch (e) {
707
+ if (e instanceof RwsError) throw e;
708
+ throw new RwsError(`getCartesianPosition failed: ${String(e)}`, 'UNKNOWN');
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Read the current Cartesian position including robot configuration flags.
714
+ * Uses /cartesian endpoint (no tool/wobj override — uses active tool/wobj).
715
+ * Returns j1/j4/j6/jx configuration integers in addition to pose.
716
+ *
717
+ * @param mechunit - Default 'ROB_1'
718
+ */
719
+ async getCartesianFull(mechunit?: string): Promise<CartesianFull> {
720
+ try {
721
+ const { body } = await this.session.get(pathCartesianFull(mechunit));
722
+ return parseCartesianFull(body);
723
+ } catch (e) {
724
+ if (e instanceof RwsError) throw e;
725
+ throw new RwsError(`getCartesianFull failed: ${String(e)}`, 'UNKNOWN');
726
+ }
727
+ }
728
+
729
+ // ─── Modules ────────────────────────────────────────────────────────────────
730
+
731
+ /**
732
+ * Download a file from the controller filesystem as a UTF-8 string.
733
+ * Use '$HOME/' prefix for the controller home directory.
734
+ *
735
+ * @param remotePath - Controller path, e.g. '$HOME/MyMod.mod'
736
+ * @returns File content as a string
737
+ */
738
+ async readFile(remotePath: string): Promise<string> {
739
+ try {
740
+ const path = pathUploadFile(remotePath);
741
+ const { body } = await this.session.get(path);
742
+ return body;
743
+ } catch (e) {
744
+ if (e instanceof RwsError) throw e;
745
+ throw new RwsError(`readFile failed: ${String(e)}`, 'UNKNOWN');
746
+ }
747
+ }
748
+
749
+ /**
750
+ * Upload a file to the controller filesystem.
751
+ * The content is uploaded as UTF-8 bytes via PUT /fileservice/{remotePath}.
752
+ * Works for any file type — RAPID modules, .cfg files, plain text, etc.
753
+ *
754
+ * @param remotePath - Controller path, e.g. '$HOME/MyMod.mod'
755
+ * @param content - File content as a string
756
+ */
757
+ async uploadFile(remotePath: string, content: string): Promise<void> {
758
+ try {
759
+ const path = pathUploadFile(remotePath);
760
+ const bytes = new TextEncoder().encode(content);
761
+ await this.session.put(path, bytes);
762
+ } catch (e) {
763
+ if (e instanceof RwsError) throw e;
764
+ throw new RwsError(`uploadFile failed: ${String(e)}`, 'UNKNOWN');
765
+ }
766
+ }
767
+
768
+ /** @deprecated use uploadFile() — same behavior, neutral name. Kept for backward compat. */
769
+ uploadModule(remotePath: string, content: string): Promise<void> {
770
+ return this.uploadFile(remotePath, content);
771
+ }
772
+
773
+ /**
774
+ * Unload a RAPID module from a task (remove it from memory).
775
+ * RAPID must be stopped before calling this.
776
+ *
777
+ * @param taskName - RAPID task name, e.g. 'T_ROB1'
778
+ * @param moduleName - Module name (without extension), e.g. 'MyProgram'
779
+ */
780
+ async unloadModule(taskName: string, moduleName: string): Promise<void> {
781
+ try {
782
+ await this.session.post(
783
+ `/rw/rapid/tasks/${encodeURIComponent(taskName)}?action=unloadmod`,
784
+ `module=${encodeURIComponent(moduleName)}`,
785
+ );
786
+ } catch (e) {
787
+ if (e instanceof RwsError) throw e;
788
+ throw new RwsError(`unloadModule failed: ${String(e)}`, 'UNKNOWN');
789
+ }
790
+ }
791
+
792
+ /**
793
+ * Load a RAPID module from the controller filesystem into a task.
794
+ * The module must have been uploaded first (see uploadModule).
795
+ *
796
+ * @param taskName - RAPID task name, e.g. 'T_ROB1'
797
+ * @param modulePath - Controller path to the module file, e.g. '$HOME/MyMod.mod'
798
+ * @throws {RwsError} code='MODULE_NOT_FOUND' if the module file does not exist
799
+ */
800
+ async loadModule(taskName: string, modulePath: string, replace = false): Promise<void> {
801
+ try {
802
+ const { path, body } = mapLoadModule(taskName, modulePath, replace);
803
+ await this.session.post(path, body);
804
+ } catch (e) {
805
+ if (e instanceof RwsError) throw e;
806
+ throw new RwsError(`loadModule failed: ${String(e)}`, 'UNKNOWN');
807
+ }
808
+ }
809
+
810
+ /**
811
+ * List the names of all modules currently loaded in a RAPID task.
812
+ *
813
+ * @param taskName - RAPID task name, e.g. 'T_ROB1'
814
+ * @returns Array of module names
815
+ */
816
+ async listModules(taskName: string): Promise<string[]> {
817
+ try {
818
+ const { body } = await this.session.get(pathListModules(taskName));
819
+ // Extract module names from <span class="name"> inside <li class="rap-module-info-li">
820
+ const matches = [
821
+ ...body.matchAll(
822
+ /<li[^>]*class="[^"]*\brap-module-info-li\b[^"]*"[^>]*>.*?<span[^>]*class="[^"]*\bname\b[^"]*"[^>]*>(.*?)<\/span>/gis,
823
+ ),
824
+ ];
825
+ return matches.map(([, name]) => name.trim()).filter(Boolean);
826
+ } catch (e) {
827
+ if (e instanceof RwsError) throw e;
828
+ throw new RwsError(`listModules failed: ${String(e)}`, 'UNKNOWN');
829
+ }
830
+ }
831
+
832
+ /**
833
+ * Download a directory listing from the controller filesystem.
834
+ * Returns entries sorted: directories first, then files.
835
+ *
836
+ * @param remotePath - Controller path, e.g. '$HOME' or '$HOME/Dispense'
837
+ */
838
+ async listDirectory(remotePath: string): Promise<FileEntry[]> {
839
+ try {
840
+ const { body } = await this.session.get(fileServicePath(remotePath));
841
+ return parseDirectory(body);
842
+ } catch (e) {
843
+ if (e instanceof RwsError) throw e;
844
+ throw new RwsError(`listDirectory failed: ${String(e)}`, 'UNKNOWN');
845
+ }
846
+ }
847
+
848
+ /**
849
+ * Delete a file from the controller filesystem.
850
+ * @param remotePath - Controller path, e.g. '$HOME/OldMod.mod'
851
+ */
852
+ async deleteFile(remotePath: string): Promise<void> {
853
+ try {
854
+ await this.session.delete(pathDeleteFile(remotePath));
855
+ } catch (e) {
856
+ if (e instanceof RwsError) throw e;
857
+ throw new RwsError(`deleteFile failed: ${String(e)}`, 'UNKNOWN');
858
+ }
859
+ }
860
+
861
+ /**
862
+ * Create a directory on the controller filesystem.
863
+ * @param parentPath - Parent directory path, e.g. '$HOME'
864
+ * @param dirName - New directory name, e.g. 'Backup'
865
+ */
866
+ async createDirectory(parentPath: string, dirName: string): Promise<void> {
867
+ try {
868
+ const path = fileServicePath(parentPath);
869
+ // RWS 1.0 fileservice expects params in the body, not the query string.
870
+ // Putting fs-action in the URL returns 400 "Invalid/No Query Parameter".
871
+ await this.session.post(
872
+ path,
873
+ `fs-action=create&fs-newname=${encodeURIComponent(dirName)}`,
874
+ );
875
+ } catch (e) {
876
+ if (e instanceof RwsError) throw e;
877
+ throw new RwsError(`createDirectory failed: ${String(e)}`, 'UNKNOWN');
878
+ }
879
+ }
880
+
881
+ /**
882
+ * Copy a file on the controller filesystem.
883
+ * @param sourcePath - Source file path, e.g. '$HOME/MyMod.mod'
884
+ * @param destPath - Destination path (full path including filename), e.g. '$HOME/Backup/MyMod.mod'
885
+ */
886
+ async copyFile(sourcePath: string, destPath: string): Promise<void> {
887
+ try {
888
+ const { path, body } = mapCopyFile(sourcePath, destPath);
889
+ await this.session.post(path, body);
890
+ } catch (e) {
891
+ if (e instanceof RwsError) throw e;
892
+ throw new RwsError(`copyFile failed: ${String(e)}`, 'UNKNOWN');
893
+ }
894
+ }
895
+
896
+ // ─── I/O signals ────────────────────────────────────────────────────────────
897
+
898
+ /**
899
+ * Read an I/O signal value.
900
+ *
901
+ * @param network - I/O network name, e.g. 'Local'
902
+ * @param device - I/O device name, e.g. 'DRV_1'
903
+ * @param name - Signal name, e.g. 'DI_1'
904
+ * @returns Signal object with name, value, type, and lvalue
905
+ */
906
+ async readSignal(network: string, device: string, name: string): Promise<Signal> {
907
+ try {
908
+ const { body } = await this.session.get(pathSignal(network, device, name));
909
+ return parseSignal(body);
910
+ } catch (e) {
911
+ if (e instanceof RwsError) throw e;
912
+ throw new RwsError(`readSignal failed: ${String(e)}`, 'UNKNOWN');
913
+ }
914
+ }
915
+
916
+ /**
917
+ * Write a value to a digital or analog output signal.
918
+ *
919
+ * @param network - I/O network name
920
+ * @param device - I/O device name
921
+ * @param name - Signal name
922
+ * @param value - New signal value, e.g. '1' (DO high), '0' (DO low), '3.14' (AO)
923
+ */
924
+ async writeSignal(network: string, device: string, name: string, value: string): Promise<void> {
925
+ try {
926
+ const { path } = mapSetSignal(network, device, name);
927
+ await this.session.post(path, `lvalue=${encodeURIComponent(value)}`);
928
+ } catch (e) {
929
+ if (e instanceof RwsError) throw e;
930
+ throw new RwsError(`writeSignal failed: ${String(e)}`, 'UNKNOWN');
931
+ }
932
+ }
933
+
934
+ /**
935
+ * List all configured I/O signals (paginated).
936
+ * The controller returns up to `limit` signals per call; use `start` to page through.
937
+ *
938
+ * @param start - Starting index (default 0)
939
+ * @param limit - Results per page (default 100)
940
+ */
941
+ async listAllSignals(start = 0, limit = 100): Promise<Signal[]> {
942
+ try {
943
+ const { body } = await this.session.get(pathAllSignals(start, limit));
944
+ return parseSignalList(body);
945
+ } catch (e) {
946
+ if (e instanceof RwsError) throw e;
947
+ throw new RwsError(`listAllSignals failed: ${String(e)}`, 'UNKNOWN');
948
+ }
949
+ }
950
+
951
+ /**
952
+ * List all configured I/O networks.
953
+ */
954
+ async listNetworks(): Promise<IoNetwork[]> {
955
+ try {
956
+ const { body } = await this.session.get(pathNetworks());
957
+ return parseNetworks(body);
958
+ } catch (e) {
959
+ if (e instanceof RwsError) throw e;
960
+ throw new RwsError(`listNetworks failed: ${String(e)}`, 'UNKNOWN');
961
+ }
962
+ }
963
+
964
+ /**
965
+ * List all I/O devices on a network.
966
+ * @param network - Network name, e.g. 'Local'
967
+ */
968
+ async listDevices(network: string): Promise<IoDevice[]> {
969
+ try {
970
+ const { body } = await this.session.get(pathDevices(network));
971
+ return parseDevices(body);
972
+ } catch (e) {
973
+ if (e instanceof RwsError) throw e;
974
+ throw new RwsError(`listDevices failed: ${String(e)}`, 'UNKNOWN');
975
+ }
976
+ }
977
+
978
+ // ─── Controller info ────────────────────────────────────────────────────────
979
+
980
+ /**
981
+ * Read RobotWare system information (version, options, system ID).
982
+ */
983
+ async getSystemInfo(): Promise<SystemInfo> {
984
+ try {
985
+ const { body } = await this.session.get(pathSystemInfo());
986
+ return parseSystemInfo(body);
987
+ } catch (e) {
988
+ if (e instanceof RwsError) throw e;
989
+ throw new RwsError(`getSystemInfo failed: ${String(e)}`, 'UNKNOWN');
990
+ }
991
+ }
992
+
993
+ /**
994
+ * Read controller hardware identity (name, ID, type, MAC address).
995
+ */
996
+ async getControllerIdentity(): Promise<ControllerIdentity> {
997
+ try {
998
+ const { body } = await this.session.get(pathControllerIdentity());
999
+ return parseControllerIdentity(body);
1000
+ } catch (e) {
1001
+ if (e instanceof RwsError) throw e;
1002
+ throw new RwsError(`getControllerIdentity failed: ${String(e)}`, 'UNKNOWN');
1003
+ }
1004
+ }
1005
+
1006
+ /**
1007
+ * Read the controller date and time.
1008
+ * Returns a ControllerClock with the datetime string in 'YYYY-MM-DD T HH:MM:SS' format (UTC).
1009
+ */
1010
+ async getControllerClock(): Promise<ControllerClock> {
1011
+ try {
1012
+ const { body } = await this.session.get(pathClockInfo());
1013
+ return parseControllerClock(body);
1014
+ } catch (e) {
1015
+ if (e instanceof RwsError) throw e;
1016
+ throw new RwsError(`getControllerClock failed: ${String(e)}`, 'UNKNOWN');
1017
+ }
1018
+ }
1019
+
1020
+ /**
1021
+ * Set the controller date and time (UTC).
1022
+ * @param year - Full year, e.g. 2024
1023
+ * @param month - Month 1–12
1024
+ * @param day - Day 1–31
1025
+ * @param hour - Hour 0–23
1026
+ * @param min - Minute 0–59
1027
+ * @param sec - Second 0–59
1028
+ */
1029
+ async setControllerClock(year: number, month: number, day: number, hour: number, min: number, sec: number): Promise<void> {
1030
+ try {
1031
+ const { path, body } = mapSetControllerClock(year, month, day, hour, min, sec);
1032
+ await this.session.put(path, new TextEncoder().encode(body));
1033
+ } catch (e) {
1034
+ if (e instanceof RwsError) throw e;
1035
+ throw new RwsError(`setControllerClock failed: ${String(e)}`, 'UNKNOWN');
1036
+ }
1037
+ }
1038
+
1039
+ // ─── Event log ──────────────────────────────────────────────────────────────
1040
+
1041
+ /**
1042
+ * Retrieve event log messages from the controller.
1043
+ * Domain 0 is the main system log (up to 1000 entries).
1044
+ * Messages are returned newest-first (LIFO order).
1045
+ *
1046
+ * @param domain - Log domain number; default 0 (main system log)
1047
+ * @param lang - Language for message text; default 'en'
1048
+ */
1049
+ async getEventLog(domain = 0, lang = 'en'): Promise<ElogMessage[]> {
1050
+ try {
1051
+ const { body } = await this.session.get(pathElogMessages(domain, lang));
1052
+ return parseElogMessages(body);
1053
+ } catch (e) {
1054
+ if (e instanceof RwsError) throw e;
1055
+ throw new RwsError(`getEventLog failed: ${String(e)}`, 'UNKNOWN');
1056
+ }
1057
+ }
1058
+
1059
+ /**
1060
+ * Clear event log messages in a specific domain.
1061
+ * @param domain - Log domain number; default 0 (main system log)
1062
+ */
1063
+ async clearEventLog(domain = 0): Promise<void> {
1064
+ try {
1065
+ const { path, body } = mapClearElogDomain(domain);
1066
+ await this.session.post(path, body);
1067
+ } catch (e) {
1068
+ if (e instanceof RwsError) throw e;
1069
+ throw new RwsError(`clearEventLog failed: ${String(e)}`, 'UNKNOWN');
1070
+ }
1071
+ }
1072
+
1073
+ /**
1074
+ * Clear ALL event log messages across all domains.
1075
+ */
1076
+ async clearAllEventLogs(): Promise<void> {
1077
+ try {
1078
+ const { path, body } = mapClearAllElogs();
1079
+ await this.session.post(path, body);
1080
+ } catch (e) {
1081
+ if (e instanceof RwsError) throw e;
1082
+ throw new RwsError(`clearAllEventLogs failed: ${String(e)}`, 'UNKNOWN');
1083
+ }
1084
+ }
1085
+
1086
+ // ─── Mastership ──────────────────────────────────────────────────────────────
1087
+
1088
+ /**
1089
+ * Request mastership on a domain.
1090
+ * Must call releaseMastership when done. Operations that modify controller state
1091
+ * (motor on/off, speed ratio, etc.) require mastership on the appropriate domain.
1092
+ *
1093
+ * @param domain - 'cfg' | 'motion' | 'rapid'
1094
+ */
1095
+ async requestMastership(domain: MastershipDomain): Promise<void> {
1096
+ try {
1097
+ const { path, body } = mapRequestMastership(domain);
1098
+ await this.session.post(path, body);
1099
+ } catch (e) {
1100
+ if (e instanceof RwsError) throw e;
1101
+ throw new RwsError(`requestMastership failed: ${String(e)}`, 'UNKNOWN');
1102
+ }
1103
+ }
1104
+
1105
+ /**
1106
+ * Release mastership on a domain.
1107
+ * Always call this after requestMastership, even if the operation failed.
1108
+ *
1109
+ * @param domain - 'cfg' | 'motion' | 'rapid'
1110
+ */
1111
+ async releaseMastership(domain: MastershipDomain): Promise<void> {
1112
+ try {
1113
+ const { path, body } = mapReleaseMastership(domain);
1114
+ await this.session.post(path, body);
1115
+ } catch (e) {
1116
+ if (e instanceof RwsError) throw e;
1117
+ throw new RwsError(`releaseMastership failed: ${String(e)}`, 'UNKNOWN');
1118
+ }
1119
+ }
1120
+
1121
+ // ─── Subscriptions ──────────────────────────────────────────────────────────
1122
+
1123
+ /**
1124
+ * Subscribe to one or more RWS resource events via WebSocket.
1125
+ *
1126
+ * @param resources - Resources to subscribe to (execution, controllerstate, signal, etc.)
1127
+ * @param handler - Called with each SubscriptionEvent as it arrives
1128
+ * @returns - Async unsubscribe function; call to cancel and clean up
1129
+ *
1130
+ * @example
1131
+ * ```ts
1132
+ * const unsubscribe = await client.subscribe(['execution'], (event) => {
1133
+ * console.log(event.resource, event.value);
1134
+ * });
1135
+ * // later...
1136
+ * await unsubscribe();
1137
+ * ```
1138
+ */
1139
+ async subscribe(
1140
+ resources: SubscriptionResource[],
1141
+ handler: (event: SubscriptionEvent) => void,
1142
+ ): Promise<() => Promise<void>> {
1143
+ try {
1144
+ return await this.subscriber.subscribe(resources, handler);
1145
+ } catch (e) {
1146
+ if (e instanceof RwsError) throw e;
1147
+ throw new RwsError(`subscribe failed: ${String(e)}`, 'UNKNOWN');
1148
+ }
1149
+ }
1150
+ }