abb-rws-client 0.2.0 → 0.7.1

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 (71) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/README.md +364 -143
  3. package/dist/HttpSession.d.ts.map +1 -1
  4. package/dist/HttpSession.js +18 -1
  5. package/dist/HttpSession.js.map +1 -1
  6. package/dist/IRWSAdapter.d.ts +421 -0
  7. package/dist/IRWSAdapter.d.ts.map +1 -0
  8. package/dist/IRWSAdapter.js +2 -0
  9. package/dist/IRWSAdapter.js.map +1 -0
  10. package/dist/Logger.d.ts +35 -0
  11. package/dist/Logger.d.ts.map +1 -0
  12. package/dist/Logger.js +24 -0
  13. package/dist/Logger.js.map +1 -0
  14. package/dist/MultiRobotManager.d.ts +56 -0
  15. package/dist/MultiRobotManager.d.ts.map +1 -0
  16. package/dist/MultiRobotManager.js +129 -0
  17. package/dist/MultiRobotManager.js.map +1 -0
  18. package/dist/RWS1Adapter.d.ts +297 -0
  19. package/dist/RWS1Adapter.d.ts.map +1 -0
  20. package/dist/RWS1Adapter.js +870 -0
  21. package/dist/RWS1Adapter.js.map +1 -0
  22. package/dist/RWS2Adapter.d.ts +20 -0
  23. package/dist/RWS2Adapter.d.ts.map +1 -0
  24. package/dist/RWS2Adapter.js +19 -0
  25. package/dist/RWS2Adapter.js.map +1 -0
  26. package/dist/ResourceMapper.d.ts +226 -2
  27. package/dist/ResourceMapper.d.ts.map +1 -1
  28. package/dist/ResourceMapper.js +286 -3
  29. package/dist/ResourceMapper.js.map +1 -1
  30. package/dist/ResponseParser.d.ts +81 -2
  31. package/dist/ResponseParser.d.ts.map +1 -1
  32. package/dist/ResponseParser.js +309 -3
  33. package/dist/ResponseParser.js.map +1 -1
  34. package/dist/RobotManager.d.ts +536 -0
  35. package/dist/RobotManager.d.ts.map +1 -0
  36. package/dist/RobotManager.js +1523 -0
  37. package/dist/RobotManager.js.map +1 -0
  38. package/dist/RwsClient.d.ts +280 -4
  39. package/dist/RwsClient.d.ts.map +1 -1
  40. package/dist/RwsClient.js +698 -10
  41. package/dist/RwsClient.js.map +1 -1
  42. package/dist/RwsClient2.d.ts +438 -0
  43. package/dist/RwsClient2.d.ts.map +1 -0
  44. package/dist/RwsClient2.js +1672 -0
  45. package/dist/RwsClient2.js.map +1 -0
  46. package/dist/WsSubscriber.d.ts.map +1 -1
  47. package/dist/WsSubscriber.js +18 -11
  48. package/dist/WsSubscriber.js.map +1 -1
  49. package/dist/XhtmlParser.d.ts +24 -0
  50. package/dist/XhtmlParser.d.ts.map +1 -0
  51. package/dist/XhtmlParser.js +56 -0
  52. package/dist/XhtmlParser.js.map +1 -0
  53. package/dist/detect.d.ts +54 -0
  54. package/dist/detect.d.ts.map +1 -0
  55. package/dist/detect.js +187 -0
  56. package/dist/detect.js.map +1 -0
  57. package/dist/index.d.ts +30 -3
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +30 -2
  60. package/dist/index.js.map +1 -1
  61. package/dist/types.d.ts +190 -3
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/types.js +1 -0
  64. package/dist/types.js.map +1 -1
  65. package/examples/01-quickstart-auto.mjs +20 -0
  66. package/examples/02-rws1-explicit.mjs +30 -0
  67. package/examples/03-rws2-explicit.mjs +26 -0
  68. package/examples/04-multi-robot.mjs +35 -0
  69. package/examples/05-remote-control-rmmp.mjs +52 -0
  70. package/examples/06-pull-module-source.mjs +38 -0
  71. package/package.json +31 -5
@@ -0,0 +1,870 @@
1
+ import * as http from 'http';
2
+ import * as crypto from 'crypto';
3
+ /**
4
+ * RWS 1.0 adapter — thin wrapper around RwsClient (abb-rws-client).
5
+ * Every method delegates directly; zero logic change from pre-adapter behavior.
6
+ * Targets ABB IRC5 controllers running RobotWare 6.x.
7
+ */
8
+ export class RWS1Adapter {
9
+ client;
10
+ creds;
11
+ constructor(client, creds) {
12
+ this.client = client;
13
+ this.creds = creds;
14
+ }
15
+ // ── Connection ──────────────────────────────────────────────────────────
16
+ connect() { return this.client.connect(); }
17
+ disconnect() { return this.client.disconnect(); }
18
+ getSessionCookie() { return this.client.getSessionCookie(); }
19
+ // ── Panel ───────────────────────────────────────────────────────────────
20
+ getControllerState() { return this.client.getControllerState(); }
21
+ setControllerState(s) { return this.client.setControllerState(s); }
22
+ getOperationMode() { return this.client.getOperationMode(); }
23
+ getSpeedRatio() { return this.client.getSpeedRatio(); }
24
+ setSpeedRatio(r) { return this.client.setSpeedRatio(r); }
25
+ getCollisionDetectionState() { return this.client.getCollisionDetectionState(); }
26
+ lockOperationMode(pin, p) { return this.client.lockOperationMode(pin, p); }
27
+ unlockOperationMode() { return this.client.unlockOperationMode(); }
28
+ setOperationMode(mode) { return this.client.setOperationMode(mode); }
29
+ // ── RAPID execution ─────────────────────────────────────────────────────
30
+ getRapidExecutionState() { return this.client.getRapidExecutionState(); }
31
+ getRapidExecutionInfo() { return this.client.getRapidExecutionInfo(); }
32
+ startRapid() { return this.client.startRapid(); }
33
+ stopRapid() { return this.client.stopRapid(); }
34
+ resetRapid() { return this.client.resetRapid(); }
35
+ setExecutionCycle(c) { return this.client.setExecutionCycle(c); }
36
+ getRapidTasks() { return this.client.getRapidTasks(); }
37
+ activateRapidTask(t) { return this.client.activateRapidTask(t); }
38
+ deactivateRapidTask(t) { return this.client.deactivateRapidTask(t); }
39
+ activateAllRapidTasks() { return this.client.activateAllRapidTasks(); }
40
+ deactivateAllRapidTasks() { return this.client.deactivateAllRapidTasks(); }
41
+ // ── RAPID modules & variables ───────────────────────────────────────────
42
+ listModules(task) { return this.client.listModules(task); }
43
+ /** RWS 1.0 module-list also exposes name + type (SysMod / ProgMod) per entry. */
44
+ async listModulesDetailed(task) {
45
+ const r = await this.rws1Get(`/rw/rapid/modules?task=${encodeURIComponent(task)}`);
46
+ return r.states
47
+ .map(s => ({ name: s['name'] ?? '', type: s['type'] ?? '' }))
48
+ .filter(m => m.name);
49
+ }
50
+ loadModule(task, path, r) { return this.client.loadModule(task, path, r); }
51
+ unloadModule(task, name) { return this.client.unloadModule(task, name); }
52
+ getRapidVariable(t, m, s) { return this.client.getRapidVariable(t, m, s); }
53
+ setRapidVariable(t, m, s, v) { return this.client.setRapidVariable(t, m, s, v); }
54
+ validateRapidValue(t, v, d) { return this.client.validateRapidValue(t, v, d); }
55
+ getRapidSymbolProperties(t, m, s) { return this.client.getRapidSymbolProperties(t, m, s); }
56
+ searchRapidSymbols(p) { return this.client.searchRapidSymbols(p); }
57
+ getActiveUiInstruction() { return this.client.getActiveUiInstruction(); }
58
+ setUiInstructionParam(su, up, v) { return this.client.setUiInstructionParam(su, up, v); }
59
+ // ── Motion ──────────────────────────────────────────────────────────────
60
+ getJointPositions(u) { return this.client.getJointPositions(u); }
61
+ getCartesianFull(u) { return this.client.getCartesianFull(u); }
62
+ /** RwsClient has no listMechunits — IRC5 always has ROB_1 as the standard mechunit. */
63
+ async listMechunits() { return ['ROB_1']; }
64
+ // ── System info ─────────────────────────────────────────────────────────
65
+ getSystemInfo() { return this.client.getSystemInfo(); }
66
+ getControllerIdentity() { return this.client.getControllerIdentity(); }
67
+ getControllerClock() { return this.client.getControllerClock(); }
68
+ setControllerClock(Y, Mo, D, H, Mi, S) {
69
+ return this.client.setControllerClock(Y, Mo, D, H, Mi, S);
70
+ }
71
+ restartController(m) { return this.client.restartController(m); }
72
+ // ── Event log ───────────────────────────────────────────────────────────
73
+ getEventLog(d, l) { return this.client.getEventLog(d, l); }
74
+ clearEventLog(d) { return this.client.clearEventLog(d); }
75
+ clearAllEventLogs() { return this.client.clearAllEventLogs(); }
76
+ // ── I/O ────────────────────────────────────────────────────────────────
77
+ listAllSignals(s, l) { return this.client.listAllSignals(s, l); }
78
+ readSignal(n, d, name) { return this.client.readSignal(n, d, name); }
79
+ writeSignal(n, d, name, v) { return this.client.writeSignal(n, d, name, v); }
80
+ listNetworks() { return this.client.listNetworks(); }
81
+ listDevices(network) { return this.client.listDevices(network); }
82
+ // ── File system ─────────────────────────────────────────────────────────
83
+ listDirectory(path) { return this.client.listDirectory(path); }
84
+ readFile(path) { return this.client.readFile(path); }
85
+ uploadFile(path, content) { return this.client.uploadModule(path, content); }
86
+ deleteFile(path) { return this.client.deleteFile(path); }
87
+ createDirectory(parent, dir) { return this.client.createDirectory(parent, dir); }
88
+ copyFile(src, dst) { return this.client.copyFile(src, dst); }
89
+ // ── Mastership ──────────────────────────────────────────────────────────
90
+ requestMastership(d) { return this.client.requestMastership(d); }
91
+ releaseMastership(d) { return this.client.releaseMastership(d); }
92
+ /** Request mastership on all domains (cfg + motion + rapid) at once. */
93
+ async requestMastershipAll() {
94
+ await this.rws1Post('/rw/mastership?action=request', '');
95
+ }
96
+ /** Release mastership on all domains at once. */
97
+ async releaseMastershipAll() {
98
+ await this.rws1Post('/rw/mastership?action=release', '');
99
+ }
100
+ /**
101
+ * RWS 1.0 doesn't expose `request-with-id` / `release-with-id` — those are
102
+ * RWS 2.0 / RobotWare 7+ additions. The `?` in the IRWSAdapter signature
103
+ * means we don't have to implement on this side; calls just throw.
104
+ */
105
+ /**
106
+ * RWS 1.0 doesn't expose a watchdog endpoint — heartbeat is RWS 2.0 only.
107
+ * The optional method on IRWSAdapter is left undefined here so callers can
108
+ * feature-detect (`if ('resetMastershipWatchdog' in adapter)`).
109
+ */
110
+ /** Read mastership status for one domain. */
111
+ async getMastershipStatus(d) {
112
+ const r = await this.rws1Get(`/rw/mastership/${d}`);
113
+ const s = r.state;
114
+ return { mastership: s?.mastership ?? 'unknown', uid: s?.uid, application: s?.application };
115
+ }
116
+ /** List mastership domains (RWS 1.0: ['cfg', 'motion', 'rapid']). */
117
+ async listMastershipDomains() {
118
+ const r = await this.rws1Get('/rw/mastership');
119
+ return r.states.map(s => s['_title']).filter(Boolean);
120
+ }
121
+ // ── Devices ────────────────────────────────────────────────────────────
122
+ async listSystemDevices() {
123
+ // RWS 1.0 nests the dev-id-li array inside _state[0].devices (different from the
124
+ // RWS 2.0 XHTML layout where each <li class="dev-id-li"> is a top-level child).
125
+ const r = await this.rws1Get('/rw/devices');
126
+ const devices = r.state?.devices ?? [];
127
+ return devices.map(d => ({
128
+ id: d['_title'] ?? '',
129
+ name: d['name'] ?? '',
130
+ }));
131
+ }
132
+ async getDeviceTree(group) {
133
+ const res = await this.client.request('GET', `/rw/devices/${encodeURIComponent(group)}?json=1`);
134
+ return res.body;
135
+ }
136
+ async listAllIoDevices() {
137
+ const r = await this.rws1Get('/rw/iosystem/devices');
138
+ return r.states.map(state => {
139
+ const s = state;
140
+ const title = s['_title'] ?? '';
141
+ return {
142
+ name: s['name'] ?? '',
143
+ network: title.split('/')[0] ?? '',
144
+ lstate: s['lstate'] ?? '',
145
+ pstate: s['pstate'] ?? '',
146
+ address: s['address'] ?? '',
147
+ };
148
+ });
149
+ }
150
+ // ── Forward kinematics ──────────────────────────────────────────────────
151
+ /**
152
+ * Forward kinematics on RWS 1.0. Same VC-license caveat as IK.
153
+ */
154
+ async calcCartesianFromJoints(joints, mechunit = 'ROB_1', tool = 'tool0', wobj = 'wobj0') {
155
+ if (!this.creds) {
156
+ throw new Error('FK requires credentials in adapter constructor');
157
+ }
158
+ const { host, port, username, password } = this.creds;
159
+ const body = [
160
+ `curr_joints=[${joints.rax_1},${joints.rax_2},${joints.rax_3},${joints.rax_4},${joints.rax_5},${joints.rax_6}]`,
161
+ `curr_ext_joints=[9E9,9E9,9E9,9E9,9E9,9E9]`,
162
+ `tool=${tool}`,
163
+ `wobj=${wobj}`,
164
+ ].join('&');
165
+ const path = `/rw/motionsystem/mechunits/${mechunit}?action=CalcRobTFromJoints&json=1`;
166
+ const result = await this.digestPost(host, port, path, body, username, password);
167
+ const state = result._embedded?._state?.[0];
168
+ if (!state) {
169
+ throw new Error('FK: no result in response');
170
+ }
171
+ return {
172
+ x: +state.x, y: +state.y, z: +state.z,
173
+ q1: +state.q1, q2: +state.q2, q3: +state.q3, q4: +state.q4,
174
+ };
175
+ }
176
+ subscribe(resources, handler) {
177
+ return this.client.subscribe(resources, handler);
178
+ }
179
+ // ── Jogging ─────────────────────────────────────────────────────────────
180
+ /** Monotonic counter required by RWS jog endpoint (rejects duplicate ccount values). */
181
+ jogCcount = 0;
182
+ async jog(params) {
183
+ if (!this.creds) {
184
+ throw new Error('Jog requires credentials — reconnect to enable');
185
+ }
186
+ const { mode, axes, speed } = params;
187
+ const mechunit = params.mechunit ?? 'ROB_1';
188
+ this.jogCcount++;
189
+ const bodyStr = [
190
+ `jogmode=${mode}`,
191
+ `mechunit=${mechunit}`,
192
+ ...axes.map((v, i) => `axis${i + 1}=${v}`),
193
+ `cjogspeed=${speed}`,
194
+ `ccount=${this.jogCcount}`,
195
+ ].join('&');
196
+ const { host, port, username, password } = this.creds;
197
+ const path = `/rw/motionsystem?action=jog&json=1`;
198
+ const result = await this.digestPost(host, port, path, bodyStr, username, password);
199
+ // Successful jog has no useful body — only check for error status.
200
+ const status = result._embedded?.status;
201
+ if (status?.msg && status.msg.length > 0 && /error|fail/i.test(status.msg)) {
202
+ throw new Error(status.msg);
203
+ }
204
+ }
205
+ // ── RWS 1.0 helper — typed wrapper around client.request() with JSON parsing ──
206
+ /**
207
+ * Generic GET that returns `_embedded._state[0]` (single resource) or [] (list).
208
+ * Most RWS 1.0 endpoints with `?json=1` return this HAL-like envelope.
209
+ * Returns empty result for HTTP 204 (no content) — common on /ctrl/options etc.
210
+ */
211
+ async rws1Get(path) {
212
+ const url = path + (path.includes('?') ? '&' : '?') + 'json=1';
213
+ const res = await this.client.request('GET', url);
214
+ if (res.status === 204 || !res.body) {
215
+ return { status: res.status, state: null, states: [], raw: null };
216
+ }
217
+ if (res.status >= 400) {
218
+ let msg = `HTTP ${res.status}`;
219
+ try {
220
+ msg = JSON.parse(res.body)._embedded?.status?.msg ?? msg;
221
+ }
222
+ catch { /* ok */ }
223
+ throw new Error(msg);
224
+ }
225
+ let parsed = {};
226
+ try {
227
+ parsed = JSON.parse(res.body);
228
+ }
229
+ catch { /* non-JSON ok */ }
230
+ const states = parsed._embedded?._state ?? [];
231
+ return { status: res.status, state: states[0] ?? null, states, raw: parsed };
232
+ }
233
+ /** Generic POST that throws on >=400, returns the parsed JSON body. */
234
+ async rws1Post(path, body) {
235
+ const url = path + (path.includes('?') ? '&' : '?') + 'json=1';
236
+ const res = await this.client.request('POST', url, body);
237
+ if (res.status >= 400) {
238
+ let msg = `HTTP ${res.status}`;
239
+ try {
240
+ msg = JSON.parse(res.body)._embedded?.status?.msg ?? msg;
241
+ }
242
+ catch { /* ok */ }
243
+ throw new Error(msg);
244
+ }
245
+ try {
246
+ return JSON.parse(res.body);
247
+ }
248
+ catch {
249
+ return null;
250
+ }
251
+ }
252
+ // ── System detail ───────────────────────────────────────────────────────
253
+ async getRobotType() {
254
+ const r = await this.rws1Get('/rw/system/robottype');
255
+ const s = r.state;
256
+ return { type: s?.['robot-type'] ?? s?.type ?? '', variant: s?.variant };
257
+ }
258
+ async getLicenseInfo() {
259
+ // RWS 1.0 path is singular `/license`. Doc 6.8 has it as plural `/licenses`
260
+ // but live IRC5 returns 404 for that — singular works.
261
+ const r = await this.rws1Get('/rw/system/license');
262
+ return { entries: r.states };
263
+ }
264
+ async listProducts() {
265
+ const r = await this.rws1Get('/rw/system/products');
266
+ return r.states;
267
+ }
268
+ async getEnergyStats() {
269
+ try {
270
+ const r = await this.rws1Get('/rw/system/energy');
271
+ return r.state ?? {};
272
+ }
273
+ catch {
274
+ return {};
275
+ }
276
+ }
277
+ // ── Return code lookup ─────────────────────────────────────────────────
278
+ async getReturnCode(code, lang = 'en') {
279
+ try {
280
+ const r = await this.rws1Get(`/rw/retcode?code=${code}&lang=${lang}`);
281
+ const s = r.state;
282
+ if (!s) {
283
+ return null;
284
+ }
285
+ return { code, title: s.title ?? '', desc: s.desc ?? '' };
286
+ }
287
+ catch {
288
+ return null;
289
+ }
290
+ }
291
+ // ── Controller detail ──────────────────────────────────────────────────
292
+ async listControllerOptions() {
293
+ try {
294
+ const r = await this.rws1Get('/ctrl/options');
295
+ return r.states.map(o => ({
296
+ name: (o.option ?? o.name ?? ''),
297
+ description: o.description,
298
+ }));
299
+ }
300
+ catch {
301
+ return [];
302
+ }
303
+ }
304
+ // ── Motion detail ──────────────────────────────────────────────────────
305
+ async getMotionChangeCount() {
306
+ const r = await this.rws1Get('/rw/motionsystem');
307
+ const s = r.state;
308
+ return Number(s?.['change-count'] ?? 0);
309
+ }
310
+ async getMotionErrorState() {
311
+ const r = await this.rws1Get('/rw/motionsystem/errorstate');
312
+ const s = r.state;
313
+ return { state: s?.['err-state'] ?? s?.state ?? 'unknown', details: s ?? undefined };
314
+ }
315
+ async getNonMotionExecution() {
316
+ const r = await this.rws1Get('/rw/motionsystem/nonmotionexecution');
317
+ const s = r.state;
318
+ return (s?.mode ?? '').toUpperCase() === 'ON';
319
+ }
320
+ async setNonMotionExecution(enabled) {
321
+ await this.rws1Post('/rw/motionsystem/nonmotionexecution?action=set', `mode=${enabled ? 'ON' : 'OFF'}`);
322
+ }
323
+ async getMechunitInfo(mechunit = 'ROB_1') {
324
+ const r = await this.rws1Get(`/rw/motionsystem/mechunits/${mechunit}`);
325
+ return r.state ?? {};
326
+ }
327
+ async getMechunitBaseFrame(mechunit = 'ROB_1') {
328
+ const r = await this.rws1Get(`/rw/motionsystem/mechunits/${mechunit}/baseframe`);
329
+ const s = r.state ?? {};
330
+ return { x: +s.x, y: +s.y, z: +s.z, q1: +s.q1, q2: +s.q2, q3: +s.q3, q4: +s.q4 };
331
+ }
332
+ async getMechunitAxes(mechunit = 'ROB_1') {
333
+ // RWS 1.0 returns 2 entries: an axis-count summary and a sub-resource link list.
334
+ // Fetch each axis individually to get its real data.
335
+ const r = await this.rws1Get(`/rw/motionsystem/mechunits/${mechunit}/axes`);
336
+ const summary = r.states.find(s => s._type === 'ms-mechunit-axes');
337
+ const count = +(summary?.axes ?? '0');
338
+ if (count === 0) {
339
+ return [];
340
+ }
341
+ const axes = [];
342
+ for (let i = 1; i <= count; i++) {
343
+ try {
344
+ const ar = await this.rws1Get(`/rw/motionsystem/mechunits/${mechunit}/axes/${i}`);
345
+ axes.push({ axis: String(i), ...(ar.state ?? {}) });
346
+ }
347
+ catch {
348
+ axes.push({ axis: String(i), error: 'unreachable' });
349
+ }
350
+ }
351
+ return axes;
352
+ }
353
+ async getActiveTool(mechunit = 'ROB_1') {
354
+ const r = await this.rws1Get(`/rw/motionsystem/mechunits/${mechunit}`);
355
+ const s = r.state ?? {};
356
+ return { name: s['tool-name'] ?? 'tool0' };
357
+ }
358
+ async getActiveWobj(mechunit = 'ROB_1') {
359
+ const r = await this.rws1Get(`/rw/motionsystem/mechunits/${mechunit}`);
360
+ const s = r.state ?? {};
361
+ return { name: s['wobj-name'] ?? 'wobj0' };
362
+ }
363
+ async getActivePayload(mechunit = 'ROB_1') {
364
+ const r = await this.rws1Get(`/rw/motionsystem/mechunits/${mechunit}`);
365
+ const s = r.state ?? {};
366
+ return { name: s['total-payload-name'] ?? s['payload-name'] ?? 'load0' };
367
+ }
368
+ // ── RAPID detail ───────────────────────────────────────────────────────
369
+ async listAliasIO() {
370
+ try {
371
+ const r = await this.rws1Get('/rw/rapid/aliasio');
372
+ return r.states.map(a => ({
373
+ alias: (a.name ?? a.alias ?? ''),
374
+ signal: (a.signal ?? a._title ?? ''),
375
+ }));
376
+ }
377
+ catch {
378
+ return [];
379
+ }
380
+ }
381
+ async getProgramPointer(task) {
382
+ try {
383
+ const r = await this.rws1Get(`/rw/rapid/tasks/${task}/pcp`);
384
+ const s = r.state ?? {};
385
+ const begin = (s.beginposition ?? '').split(',');
386
+ return {
387
+ module: s.modulename ?? s.modulemame ?? s.module,
388
+ routine: s.routinename ?? s.routine,
389
+ row: begin[0] ? +begin[0] : undefined,
390
+ col: begin[1] ? +begin[1] : undefined,
391
+ };
392
+ }
393
+ catch {
394
+ return {};
395
+ }
396
+ }
397
+ async getMotionPointer(task) {
398
+ // RWS 1.0 path is /rw/rapid/tasks/{task}/motion (per official doc 6.7)
399
+ try {
400
+ const r = await this.rws1Get(`/rw/rapid/tasks/${task}/motion`);
401
+ const s = r.state ?? {};
402
+ return {
403
+ module: s.modulename ?? s.modulemame ?? s.module,
404
+ routine: s.routinename ?? s.routine,
405
+ };
406
+ }
407
+ catch {
408
+ return {};
409
+ }
410
+ }
411
+ // ── CFG database ───────────────────────────────────────────────────────
412
+ async listCfgDomains() {
413
+ const r = await this.rws1Get('/rw/cfg');
414
+ return r.states.map(d => (d._title ?? d.name)).filter(Boolean);
415
+ }
416
+ async listCfgTypes(domain) {
417
+ const types = [];
418
+ let path = `/rw/cfg/${domain}`;
419
+ let pages = 0;
420
+ while (path && pages < 50) {
421
+ const r = await this.rws1Get(path);
422
+ const ts = r.states.map(t => (t._title ?? t.name)).filter(Boolean);
423
+ types.push(...ts);
424
+ // RWS 1.0 pagination: `_links.next.href` in the response
425
+ const links = r.raw?._links;
426
+ const next = links?.next?.href;
427
+ if (next && pages < 49) {
428
+ path = '/rw/cfg/' + next.replace(/^\/+/, '').replace(/^cfg\//, '').replace(/&amp;/g, '&').replace(/[?&]json=1/, '');
429
+ }
430
+ else {
431
+ path = '';
432
+ }
433
+ pages++;
434
+ }
435
+ return types;
436
+ }
437
+ async listCfgInstances(domain, type) {
438
+ try {
439
+ const r = await this.rws1Get(`/rw/cfg/${domain}/${type}/instances`);
440
+ return r.states.map(i => (i._title ?? i.name)).filter(Boolean);
441
+ }
442
+ catch {
443
+ return [];
444
+ }
445
+ }
446
+ async getCfgInstance(domain, type, instance) {
447
+ // RWS 1.0 inlines all attribute data in the instance-list response. The single-instance
448
+ // GET also works at `/instances/{name}`. Use the list call (one HTTP request) and find
449
+ // by _title — also handles instance names with spaces/special chars correctly.
450
+ const r = await this.rws1Get(`/rw/cfg/${domain}/${type}/instances`);
451
+ const target = r.states.find(s => s._title === instance);
452
+ if (!target) {
453
+ return {};
454
+ }
455
+ const out = {};
456
+ // Attributes can come in two shapes on RWS 1.0:
457
+ // 1. Inline `attrib` array of { _title: name, value }
458
+ // 2. Direct keyed properties on the state object
459
+ const attribs = target.attrib;
460
+ if (Array.isArray(attribs)) {
461
+ for (const a of attribs) {
462
+ if (a._title) {
463
+ out[a._title] = String(a.value ?? '');
464
+ }
465
+ }
466
+ }
467
+ // Always include direct properties (rdonly, instanceid, etc.) — useful metadata.
468
+ for (const [k, v] of Object.entries(target)) {
469
+ if (k.startsWith('_') || k === 'attrib') {
470
+ continue;
471
+ }
472
+ if (typeof v === 'string') {
473
+ out[k] = v;
474
+ }
475
+ }
476
+ return out;
477
+ }
478
+ // ── Backup ─────────────────────────────────────────────────────────────
479
+ async listBackups() {
480
+ try {
481
+ const entries = await this.client.listDirectory('$BACKUP');
482
+ return entries.filter(e => e.type === 'dir').map(e => ({
483
+ name: e.name,
484
+ created: e.created,
485
+ }));
486
+ }
487
+ catch {
488
+ return [];
489
+ }
490
+ }
491
+ async getBackupStatus() {
492
+ try {
493
+ const r = await this.rws1Get('/ctrl/backup');
494
+ const s = r.state ?? {};
495
+ const phase = s['progress-state'] ?? s.phase ?? '';
496
+ return {
497
+ active: phase !== '' && phase !== 'idle' && phase !== 'finished',
498
+ progress: s.progress ? +s.progress : undefined,
499
+ phase,
500
+ };
501
+ }
502
+ catch {
503
+ return { active: false };
504
+ }
505
+ }
506
+ // ── RMMP ───────────────────────────────────────────────────────────────
507
+ async getRmmpPrivilege() {
508
+ try {
509
+ const r = await this.rws1Get('/users/rmmp');
510
+ const s = r.state ?? {};
511
+ const priv = s.privilege ?? 'none';
512
+ const heldByMe = (s.rmmpheldbyme ?? 'false').toLowerCase() === 'true';
513
+ if (priv === 'none' || priv.startsWith('pending')) {
514
+ return priv;
515
+ }
516
+ return heldByMe ? priv : 'none';
517
+ }
518
+ catch {
519
+ return 'none';
520
+ }
521
+ }
522
+ async requestRmmp(level = 'modify') {
523
+ await this.rws1Post('/users/rmmp', `privilege=${level}`);
524
+ }
525
+ // ── Stage 7: Backup / Restore / Progress (5 methods) ───────────────────
526
+ async createBackup(name) {
527
+ // Async — controller returns 202 + Location header pointing to /progress/{id}.
528
+ // Caller polls getProgress() to track completion.
529
+ await this.rws1Post('/ctrl/backup?action=backup', `backup=$BACKUP/${encodeURIComponent(name)}`);
530
+ }
531
+ async restoreBackup(name) {
532
+ await this.rws1Post('/ctrl/backup?action=restore', `backup=$BACKUP/${encodeURIComponent(name)}`);
533
+ }
534
+ async listProgress() {
535
+ try {
536
+ const r = await this.rws1Get('/progress');
537
+ return r.states.map(p => ({
538
+ id: (p._title ?? p.id ?? ''),
539
+ state: (p.state ?? ''),
540
+ }));
541
+ }
542
+ catch {
543
+ return [];
544
+ }
545
+ }
546
+ async getProgress(id) {
547
+ try {
548
+ const r = await this.rws1Get(`/progress/${encodeURIComponent(id)}`);
549
+ const s = r.state;
550
+ if (!s) {
551
+ return null;
552
+ }
553
+ return { state: s.state ?? '', details: s };
554
+ }
555
+ catch {
556
+ return null;
557
+ }
558
+ }
559
+ // ── Stage 8: DIPC (6 methods) ──────────────────────────────────────────
560
+ async listDipcQueues() {
561
+ try {
562
+ const r = await this.rws1Get('/rw/dipc');
563
+ return r.states.map(q => ({
564
+ name: (q._title ?? q['queue-name'] ?? ''),
565
+ size: q['queue-size'] !== undefined ? +q['queue-size'] : undefined,
566
+ }));
567
+ }
568
+ catch {
569
+ return [];
570
+ }
571
+ }
572
+ async createDipcQueue(name, options = {}) {
573
+ const parts = [`dipc-queue-name=${encodeURIComponent(name)}`];
574
+ if (options.maxsize) {
575
+ parts.push(`dipc-max-size=${options.maxsize}`);
576
+ }
577
+ if (options.maxmessages) {
578
+ parts.push(`dipc-max-number-of-messages=${options.maxmessages}`);
579
+ }
580
+ await this.rws1Post('/rw/dipc?action=create', parts.join('&'));
581
+ }
582
+ async sendDipcMessage(queue, payload, type = 'string') {
583
+ const typeCode = type === 'string' ? '0' : type === 'num' ? '1' : type === 'dnum' ? '2' : '3';
584
+ await this.rws1Post(`/rw/dipc/${encodeURIComponent(queue)}?action=send`, `dipc-src-queue-name=${encodeURIComponent(queue)}&dipc-cmd=111&dipc-data=${encodeURIComponent(payload)}&dipc-msgtype=${typeCode}`);
585
+ }
586
+ async readDipcMessage(queue) {
587
+ try {
588
+ const r = await this.rws1Get(`/rw/dipc/${encodeURIComponent(queue)}?action=read`);
589
+ const s = r.state;
590
+ if (!s || !s['dipc-data']) {
591
+ return null;
592
+ }
593
+ return { payload: s['dipc-data'], type: s['dipc-msgtype'] ?? 'string' };
594
+ }
595
+ catch {
596
+ return null;
597
+ }
598
+ }
599
+ async removeDipcQueue(name) {
600
+ await this.client.request('DELETE', `/rw/dipc/${encodeURIComponent(name)}?json=1`);
601
+ }
602
+ // ── Stage 9: Safety (5 methods) ────────────────────────────────────────
603
+ async getSafetyStatus() {
604
+ try {
605
+ const r = await this.rws1Get('/ctrl/safety');
606
+ const s = r.state;
607
+ return { state: s?.state ?? 'unavailable', details: s ?? undefined };
608
+ }
609
+ catch {
610
+ return { state: 'unavailable' };
611
+ }
612
+ }
613
+ async listSafetyZones() {
614
+ try {
615
+ const r = await this.rws1Get('/ctrl/safety/zones');
616
+ return r.states;
617
+ }
618
+ catch {
619
+ return [];
620
+ }
621
+ }
622
+ async runCyclicBrakeCheck() {
623
+ await this.rws1Post('/ctrl/safety/cyclic-brake-check', '');
624
+ }
625
+ // ── Stage 10: Virtual time (3 methods, VC-only) ────────────────────────
626
+ async getVirtualTime() {
627
+ try {
628
+ // RWS 1.0 has /ctrl/virtualtime as a directory; query each sub-resource.
629
+ const fetch = async (sub) => {
630
+ try {
631
+ const r = await this.rws1Get(`/ctrl/virtualtime/${sub}`);
632
+ return r.state ?? {};
633
+ }
634
+ catch {
635
+ return {};
636
+ }
637
+ };
638
+ const [time, state, speed] = await Promise.all([fetch('vttime'), fetch('vtstate'), fetch('vtspeed')]);
639
+ return {
640
+ time: Number(time.vtcounter ?? time.time ?? 0),
641
+ running: (state.vtcurrstate ?? state.state ?? '').toLowerCase() === 'running',
642
+ speed: speed.vtcurrspeed !== undefined ? +speed.vtcurrspeed : undefined,
643
+ };
644
+ }
645
+ catch {
646
+ return { time: 0, running: false };
647
+ }
648
+ }
649
+ async setVirtualTimeRunning(running) {
650
+ await this.rws1Post(`/ctrl/virtualtime/vtstate?action=${running ? 'run' : 'pause'}`, '');
651
+ }
652
+ async setVirtualTimeScale(scale) {
653
+ await this.rws1Post('/ctrl/virtualtime/vtspeed?action=set', `vtcurrspeed=${scale}`);
654
+ }
655
+ // ── Stage 11: Vision (5 methods) ───────────────────────────────────────
656
+ async listVisionSystems() {
657
+ try {
658
+ const r = await this.rws1Get('/rw/vision');
659
+ return r.states.map(v => ({
660
+ name: (v._title ?? v.name ?? ''),
661
+ status: v.status,
662
+ }));
663
+ }
664
+ catch {
665
+ return [];
666
+ }
667
+ }
668
+ async getVisionSystemInfo(name) {
669
+ try {
670
+ const r = await this.rws1Get(`/rw/vision/${encodeURIComponent(name)}`);
671
+ return r.state ?? {};
672
+ }
673
+ catch {
674
+ return {};
675
+ }
676
+ }
677
+ async triggerVisionJob(system) {
678
+ await this.rws1Post(`/rw/vision/${encodeURIComponent(system)}?action=trigger`, '');
679
+ }
680
+ // ── Stage 12: RAPID extras (4 methods) ─────────────────────────────────
681
+ async saveModule(task, moduleName, filepath) {
682
+ await this.rws1Post(`/rw/rapid/tasks/${task}?action=savemod`, `name=${encodeURIComponent(moduleName)}&filepath=${encodeURIComponent(filepath)}`);
683
+ }
684
+ async listModuleRoutines(task, moduleName) {
685
+ try {
686
+ const r = await this.rws1Get(`/rw/rapid/modules/${task}/${moduleName}/routines`);
687
+ return r.states.map(rt => ({
688
+ name: (rt.name ?? rt._title ?? ''),
689
+ type: (rt.type ?? ''),
690
+ }));
691
+ }
692
+ catch {
693
+ return [];
694
+ }
695
+ }
696
+ async listBreakpoints(task) {
697
+ try {
698
+ // Per official doc: CCRapidBreakPointResource — exact path varies by RW version.
699
+ const r = await this.rws1Get(`/rw/rapid/tasks/${task}/breakpoints`);
700
+ return r.states.map(b => ({
701
+ module: (b.module ?? b.modulename ?? ''),
702
+ row: +(b['begin-position-row'] ?? b.row ?? 0),
703
+ col: b['begin-position-col'] !== undefined ? +b['begin-position-col'] : undefined,
704
+ }));
705
+ }
706
+ catch {
707
+ return [];
708
+ }
709
+ }
710
+ async holdToRun(task, action) {
711
+ await this.rws1Post(`/rw/rapid/tasks/${task}?action=holdtorun`, `action=${action}`);
712
+ }
713
+ async startProductionMode() {
714
+ await this.rws1Post('/rw/rapid/execution?action=start-prod', '');
715
+ }
716
+ // ── Stage 13: Network / time / compatibility (5 methods) ──────────────
717
+ async getNetworkConfig() {
718
+ try {
719
+ const r = await this.rws1Get('/ctrl/network');
720
+ return r.state ?? {};
721
+ }
722
+ catch {
723
+ return {};
724
+ }
725
+ }
726
+ async getDnsConfig() {
727
+ try {
728
+ const r = await this.rws1Get('/ctrl/network/dns');
729
+ return r.state ?? {};
730
+ }
731
+ catch {
732
+ return {};
733
+ }
734
+ }
735
+ async getRoutingTable() {
736
+ try {
737
+ const r = await this.rws1Get('/ctrl/network/routes');
738
+ return r.states;
739
+ }
740
+ catch {
741
+ return [];
742
+ }
743
+ }
744
+ async getTimezone() {
745
+ try {
746
+ const r = await this.rws1Get('/ctrl/clock/timezone');
747
+ const s = r.state ?? {};
748
+ return { tz: s.timezone ?? '', raw: s };
749
+ }
750
+ catch {
751
+ return { tz: '', raw: {} };
752
+ }
753
+ }
754
+ async getCompatibility() {
755
+ try {
756
+ const r = await this.rws1Get('/ctrl/compatible');
757
+ const s = r.state ?? {};
758
+ return { compatible: (s.compatible ?? '').toLowerCase() === 'true', details: s };
759
+ }
760
+ catch {
761
+ return { compatible: false };
762
+ }
763
+ }
764
+ // ── Stage 14: Set mechunit / robtarget for jogging (2 methods) ────────
765
+ async setMechunitForJogging(mechunit) {
766
+ await this.rws1Post('/rw/motionsystem?action=set-mechunit', `mechunit=${encodeURIComponent(mechunit)}`);
767
+ }
768
+ async setRobtargetForJogging(target) {
769
+ const t = target;
770
+ await this.rws1Post('/rw/motionsystem?action=set-target', `x=${t.x}&y=${t.y}&z=${t.z}&q1=${t.q1}&q2=${t.q2}&q3=${t.q3}&q4=${t.q4}`);
771
+ }
772
+ // ── Inverse kinematics ──────────────────────────────────────────────────
773
+ async calcJointsFromCartesian(pos, seedJoints, mechunit = 'ROB_1') {
774
+ if (!this.creds) {
775
+ throw new Error('IK requires credentials — reconnect to enable');
776
+ }
777
+ const { host, port, username, password } = this.creds;
778
+ const seed = seedJoints
779
+ ? `[${seedJoints.rax_1},${seedJoints.rax_2},${seedJoints.rax_3},${seedJoints.rax_4},${seedJoints.rax_5},${seedJoints.rax_6}]`
780
+ : '[0,0,0,0,0,0]';
781
+ const bodyStr = [
782
+ `curr_position=[${pos.x},${pos.y},${pos.z}]`,
783
+ `curr_orientation=[${pos.q1},${pos.q2},${pos.q3},${pos.q4}]`,
784
+ `curr_ext_joints=[9E9,9E9,9E9,9E9,9E9,9E9]`,
785
+ `old_rob_joints=${seed}`,
786
+ `old_ext_joints=[9E9,9E9,9E9,9E9,9E9,9E9]`,
787
+ `robot_fixed_object=false`,
788
+ `tool_frame_position=[0,0,0]`,
789
+ `tool_frame_orientation=[1,0,0,0]`,
790
+ `wobj_frame_position=[0,0,0]`,
791
+ `wobj_frame_orientation=[1,0,0,0]`,
792
+ `robot_configuration=[0,0,0,0]`,
793
+ `elog_at_error=false`,
794
+ ].join('&');
795
+ const path = `/rw/motionsystem/mechunits/${mechunit}?action=CalcJointsFromPose&json=1`;
796
+ const result = await this.digestPost(host, port, path, bodyStr, username, password);
797
+ // RWS 1.0 IK response shape: { _embedded: { _state: [{ rax_1, rax_2, ... }] } }
798
+ const state = result._embedded?._state?.[0];
799
+ if (!state) {
800
+ throw new Error('IK: no result in response');
801
+ }
802
+ return {
803
+ rax_1: +state.rax_1, rax_2: +state.rax_2, rax_3: +state.rax_3,
804
+ rax_4: +state.rax_4, rax_5: +state.rax_5, rax_6: +state.rax_6,
805
+ };
806
+ }
807
+ digestPost(host, port, path, body, user, pass) {
808
+ // Two-step Digest: first GET challenge, then POST with auth header
809
+ return new Promise((resolve, reject) => {
810
+ // Step 1: send no-auth POST to get the 401 challenge
811
+ const challenge = http.request({ method: 'POST', hostname: host, port, path, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }, res1 => {
812
+ const wwwAuth = (res1.headers['www-authenticate'] ?? '');
813
+ res1.resume();
814
+ if (res1.statusCode !== 401) {
815
+ reject(new Error(`IK: expected 401 challenge, got ${res1.statusCode}`));
816
+ return;
817
+ }
818
+ // Parse Digest challenge
819
+ const realm = wwwAuth.match(/realm="([^"]+)"/)?.[1] ?? '';
820
+ const nonce = wwwAuth.match(/nonce="([^"]+)"/)?.[1] ?? '';
821
+ const qop = wwwAuth.match(/qop="([^"]+)"/)?.[1] ?? 'auth';
822
+ // Build auth header (RFC 2617)
823
+ const cnonce = crypto.randomBytes(8).toString('hex');
824
+ const nc = '00000001';
825
+ const ha1 = crypto.createHash('md5').update(`${user}:${realm}:${pass}`).digest('hex');
826
+ const ha2 = crypto.createHash('md5').update(`POST:${path.split('?')[0]}`).digest('hex');
827
+ const respH = crypto.createHash('md5').update(`${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`).digest('hex');
828
+ const authH = `Digest username="${user}", realm="${realm}", nonce="${nonce}", uri="${path.split('?')[0]}", qop=${qop}, nc=${nc}, cnonce="${cnonce}", response="${respH}"`;
829
+ // Step 2: POST with Digest auth
830
+ const encoded = Buffer.from(body);
831
+ const req2 = http.request({
832
+ method: 'POST', hostname: host, port, path,
833
+ headers: {
834
+ Authorization: authH,
835
+ 'Content-Type': 'application/x-www-form-urlencoded',
836
+ 'Content-Length': String(encoded.length),
837
+ Accept: 'application/json',
838
+ },
839
+ }, res2 => {
840
+ const chunks = [];
841
+ res2.on('data', (c) => chunks.push(c));
842
+ res2.on('end', () => {
843
+ const raw = Buffer.concat(chunks).toString('utf8');
844
+ if ((res2.statusCode ?? 0) >= 400) {
845
+ let msg = `IK HTTP ${res2.statusCode}`;
846
+ try {
847
+ msg = JSON.parse(raw)._embedded?.status?.msg ?? msg;
848
+ }
849
+ catch { /* ok */ }
850
+ reject(new Error(msg));
851
+ return;
852
+ }
853
+ try {
854
+ resolve(JSON.parse(raw));
855
+ }
856
+ catch {
857
+ reject(new Error('IK: could not parse response'));
858
+ }
859
+ });
860
+ });
861
+ req2.on('error', reject);
862
+ req2.write(encoded);
863
+ req2.end();
864
+ });
865
+ challenge.on('error', reject);
866
+ challenge.end();
867
+ });
868
+ }
869
+ }
870
+ //# sourceMappingURL=RWS1Adapter.js.map