abb-rws-client 0.7.1 → 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 +169 -0
  2. package/README.md +61 -24
  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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,175 @@ All notable changes to `abb-rws-client` are documented here.
4
4
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.0.0] — 2026-07-09
8
+
9
+ The library is feature-complete for both controller generations and every wire
10
+ recipe below has been verified against live controllers (RW 6.16 IRC5 +
11
+ RW 7.21 OmniCore). Time to call it 1.0.
12
+
13
+ ### Added
14
+
15
+ - **mDNS/Bonjour controller discovery** — `RobotManager.discoverControllersMdns()`
16
+ finds every ABB controller (real or virtual) announcing on the local network:
17
+ zero-dependency DNS-SD implementation, returns system name, host, RWS port,
18
+ RobotWare version, system GUID, and a protocol classification. No more port
19
+ scanning to find RobotStudio's randomly-assigned VC ports.
20
+ - **HAL JSON parsing for RWS 2.0** — GETs negotiate
21
+ `application/hal+json;v=2.0` (officially supported; live-verified) with an
22
+ automatic, remembered per-controller fallback to XHTML for older RobotWare 7
23
+ releases. Byte-identical results across both representations verified live
24
+ over seven read families.
25
+ - **Simulation panel (virtual controllers, RW7)** — `simEmergencyStop()`,
26
+ `simResetEmergencyStop()`, `simGeneralStop()`, `simAutoStop()`,
27
+ `simEnableSwitch(on)`, and `teleportMechunit(mechunit, joints)`. Drive the
28
+ E-stop/guard-stop chain and reposition the simulated robot from the API —
29
+ every recipe converged from live controller validation errors, including the
30
+ undocumented inverted `state=off`-engages polarity.
31
+ - **`RWS1Adapter.saveModule` migrated off a dead endpoint** — the legacy
32
+ `?action=savemod` form returns a blanket 400 on RW6 regardless of body; the
33
+ method now uses the live-verified `?action=save` recipe (and percent-encodes
34
+ the destination path).
35
+
36
+ ### Changed
37
+
38
+ - Published packages now include `src/` so the shipped source maps resolve;
39
+ the `exports` map lists `types` first for `nodenext` consumers.
40
+ - 169 new unit tests (368 total): every previously-untested `ResourceMapper`
41
+ builder and `ResponseParser` parser, the RWS 1.0 digest/cookie/pacing
42
+ request path, the mDNS wire parser, HAL/XHTML parity, and the simulation
43
+ panel.
44
+
45
+ ## [0.8.0] — 2026-07-09
46
+
47
+ ### Fixed
48
+
49
+ - **RWS 2.0 real-time subscriptions actually work now.** The WebSocket handshake
50
+ offered `robapi2_subscription` — the RWS **1.0** subprotocol name — and
51
+ RobotWare 7 rejects it with HTTP 400, so every OmniCore connection silently
52
+ fell back to polling since the feature shipped. The client now offers the
53
+ official `rws_subscription` (RWS 2.0 manual 3HAC073675-001). Live-verified on
54
+ RW 7.21: handshake 101, real event frames delivered.
55
+ - **RWS 2.0 WebSocket drops no longer kill live updates for the session** — the
56
+ 25 s ping interval is cleared on close, the client re-registers the
57
+ subscription with bounded exponential backoff, and when it finally gives up
58
+ it tells the owner (see `onLost` below) so `RobotManager` can restore fast
59
+ polling instead of idling at the slow cadence forever.
60
+ - **`getModuleSource` works for modules with no backing file** (loaded from
61
+ `.pgf`, RobotStudio, or the FlexPendant — [abb-rws-vscode#3](https://github.com/ichbinmeraj/abb-rws-vscode/issues/3)):
62
+ both protocols now fall back to saving the module to the controller's TEMP
63
+ volume, reading it, and deleting it. On RWS 1.0 the save round-trip is the
64
+ primary path so a stale `$HOME` copy can never shadow program memory.
65
+ Wire recipes live-verified on RW 6.16 + RW 7.21 (no mastership needed).
66
+ - **CFG writes were broken on both protocols**: `setCfgInstance` POSTed a
67
+ non-existent RWS 2.0 path (missing `/instances/`) and sent plain values where
68
+ RobotWare 7 requires the bracket representation (`Attr=[value,1]`);
69
+ `createCfgInstance` used an endpoint that does not exist (`…/{i}/create`) —
70
+ the real flow is `instances/create-default` + set. RWS 1.0 had no CFG write
71
+ support at all; the adapter now implements set/create/remove with the
72
+ plain-value forms. Full create → set → readback → delete cycles live-verified
73
+ on both controllers.
74
+ - **`probeProtocol` no longer misdetects arbitrary web servers as RWS 2.0** —
75
+ classification now requires a Digest (RWS 1.0) or Basic (RWS 2.0) challenge;
76
+ Bearer challenges and plain 200 responses are rejected.
77
+ - **`createAdapter` gained the Default-User fallback** `createClient` already
78
+ had — and the fallback predicate now keys on the typed `AUTH_FAILED` error
79
+ code (the old message regex never matched RWS 1.0 login failures).
80
+ - **`connect()`/`disconnect()` races**: disconnecting during an in-flight
81
+ connect no longer resurrects timers or subscriptions; a second `connect()`
82
+ with different host/credentials supersedes the in-flight attempt instead of
83
+ being silently coalesced; a poll that loses the race with `disconnect()` can
84
+ no longer write stale task state back into a cleared manager.
85
+ - **`writeSignal` with unknown network/device** now throws a descriptive
86
+ `RwsError` instead of firing a malformed `/signals///…` request.
87
+ - **Fileservice paths are percent-encoded per segment on both protocols** —
88
+ file names containing space, `#`, or `%` no longer break (or truncate at the
89
+ `#`) list/read/upload/delete/copy operations. `$HOME`/`$TEMP` prefixes stay
90
+ literal.
91
+ - **`RWS1Adapter.listMechunits` returns the controller's real mechunit list**
92
+ instead of a hardcoded `['ROB_1']`.
93
+ - **Session-cookie store writes are atomic** (temp file + rename), so
94
+ concurrent connects can't drop each other's entries.
95
+ - **Digest `qop=auth-int`** is now rejected with a clear error instead of
96
+ silently hashing as `auth` and failing authentication downstream.
97
+ - **Class-name minification safety**: protocol detection for config
98
+ persistence uses `instanceof`, not `constructor.name`.
99
+
100
+ ### Added
101
+
102
+ - **`RobotManagerOptions`** — `new RobotManager({ refreshIntervalMs, strictTls })`
103
+ and `MultiRobotManager.fromConfigs(configs, options)`:
104
+ - `refreshIntervalMs` (default 1000, min 200) controls the polling cadence;
105
+ the subscription-active slow poll scales at 5×.
106
+ - `strictTls` (default false) turns real TLS certificate verification on for
107
+ controllers with proper certificates. Off by default because controllers
108
+ ship self-signed certs.
109
+ - **`RwsClient2` constructor options** `{ timeout, rejectUnauthorized }` — the
110
+ per-request timeout is finally configurable (was hardcoded 10 s), and callers
111
+ can opt into certificate verification.
112
+ - **`onLost` subscription callback** on the adapter `subscribe` surface —
113
+ invoked once when the event stream is terminally lost so callers can degrade
114
+ gracefully.
115
+ - **CI workflow** (Node 18/20/22 matrix: build, tests, lint) and `SECURITY.md`.
116
+
117
+ ## [0.7.3] — 2026-07-03
118
+
119
+ ### Fixed
120
+
121
+ - **TLS bypass now applied per-request, not only on the HTTP agent** — fixes
122
+ connections to real controllers from inside VS Code
123
+ ([abb-rws-vscode#2](https://github.com/ichbinmeraj/abb-rws-vscode/issues/2)).
124
+ VS Code's extension host patches Node's `http`/`https` modules and replaces
125
+ custom agents for non-localhost targets, which silently dropped the
126
+ agent-level `rejectUnauthorized: false` and re-enabled certificate
127
+ verification — every real OmniCore (self-signed cert) then failed with
128
+ `self signed certificate`. Localhost VCs were never affected because the
129
+ extension host doesn't intercept localhost traffic. The setting is now also
130
+ set on each request's options in `RwsClient2.req()`, the subscription POST,
131
+ `RobotManager` port probing, and `detect.probeProtocol()`.
132
+ - **Examples 05 & 06 rewritten against the real API** — they previously used an
133
+ options-object `RwsClient2` constructor and `RobotManager` methods that never
134
+ existed. Both now use `new RobotManager()` + `connect(host, user, pass, port?)`.
135
+ - **`repository`/`homepage`** now point at the actual GitHub org (`ichbinmeraj`);
136
+ the previous links 404'd from the npm package page.
137
+ - Stale `types.ts` header no longer claims the package is RWS 1.0-only.
138
+
139
+ ### Added
140
+
141
+ - **`prepack` guard** — `npm pack`/`npm publish` now runs the build and the full
142
+ 116-test suite first, so a stale or broken artifact cannot be packed.
143
+
144
+ ## [0.7.2] — 2026-05-08
145
+
146
+ ### Documentation
147
+
148
+ - **Fixed the embarrassing compatibility table** that incorrectly claimed
149
+ RobotWare 7.x / OmniCore was "Not compatible". The package has supported
150
+ both protocols since v0.7.0 — the table was a stale leftover from when
151
+ only RWS 1.0 shipped. Now correctly shows both ✅, with live-tested
152
+ versions called out (RW7.21 + RW6.16).
153
+ - **Documented `RobotManager` higher-level surface** — the README's
154
+ API-reference tables only covered the protocol-level `RwsClient`, so
155
+ callers couldn't see what `RobotManager` adds on top: RMMP, mastership
156
+ status, opmode auto-routing, backup, FK, service-routine call,
157
+ tool/wobj activation, CFG write surface, DIPC messaging, file volumes,
158
+ module source, compress, value validation. New section lists all of
159
+ these with descriptions.
160
+ - **Clarified RWS 2.0 subscription quirks** — the polling-fallback
161
+ paragraph now names the specific RWS 2.0 VC `robapi2_subscription`
162
+ rejection that triggers the fallback, rather than implying it's
163
+ always 5s polling.
164
+ - **Session-pool clarifications** — IRC5's 70-session number was being
165
+ presented as universal; called out OmniCore's also-finite-but-different
166
+ pool with the empirical 503-once-full behaviour we observed during
167
+ protocol probes.
168
+ - **`RobotManager` polling cadence** correctly described as hybrid:
169
+ 5 s when WS subscriptions handle state changes, 1 s when polling
170
+ covers everything.
171
+
172
+ No code changes — pure README updates. `RwsClient`, `RwsClient2`,
173
+ `RobotManager`, `MultiRobotManager`, `createClient`, all examples and
174
+ unit tests are bit-for-bit identical to v0.7.1.
175
+
7
176
  ## [0.7.1] — 2026-05-07
8
177
 
9
178
  ### Added
package/README.md CHANGED
@@ -25,7 +25,7 @@ Prefer a GUI? The companion VS Code extension gives you live status, motion data
25
25
  - **Connection lifecycle** — `RobotManager` handles port discovery, polling, WebSocket subscriptions with polling fallback, reconnect-on-failure
26
26
  - **Typed adapter pattern** — `IRWSAdapter` lets you write code that works across both protocols
27
27
  - **WebSocket subscriptions** for real-time events (panel state, RAPID exec, signals, persvar, elog, jointtarget, …)
28
- - Session cookie management (avoids the controller's 70-session pool fill)
28
+ - Session cookie management (IRC5: avoids the controller's 70-session pool fill; OmniCore: avoids 503 lockout from session-pool exhaustion)
29
29
  - Automatic `/logout` on disconnect to release server-side mastership and free the session slot
30
30
  - Request rate limiting (< 20 req/sec)
31
31
  - Fully typed public API — every method throws `RwsError` with a typed `code`
@@ -159,7 +159,9 @@ console.log('joints:', await client.getJointPositions());
159
159
  // (`/rw/rapid/symbol/{symburl}/data`); the method shape is the same.
160
160
  const tool0 = await client.getRapidVariable('T_ROB1', 'BASE', 'tool0');
161
161
 
162
- // WebSocket subscriptions over `robapi2_subscription` subprotocol.
162
+ // WebSocket subscriptions over the `rws_subscription` subprotocol
163
+ // (RWS 1.0 uses `robapi2_subscription` — the names are NOT interchangeable:
164
+ // RobotWare 7 rejects the 1.0 name with HTTP 400).
163
165
  const unsubscribe = await client.subscribe(
164
166
  ['controllerstate', 'execution'],
165
167
  (event) => console.log(event.resource, '=', event.value),
@@ -180,8 +182,10 @@ These are documented because they bite anyone who tries to write an RWS 2.0 clie
180
182
  - **File service home** is `'HOME'`, not `'$HOME'`.
181
183
  - **Symbol API path is suffix-style** — `/rw/rapid/symbol/{symburl}/data` (RWS 1.0 puts `/data` at the front).
182
184
  - **Module unload** is `POST /rw/rapid/tasks/{task}/unloadmod` with body, NOT `DELETE` on the module URL (returns 405).
183
- - **Self-signed TLS** on virtual controllers library uses `rejectUnauthorized: false` for HTTPS.
184
- - **WebSocket subscription URL** comes from the `Location` header (real hardware) or the XHTML body (VC). Subprotocol: `robapi2_subscription`.
185
+ - **Self-signed TLS** everywhere controllers ship self-signed certs, so certificate
186
+ verification is off by default. Pass `strictTls: true` (`RobotManagerOptions`) or
187
+ `rejectUnauthorized: true` (`RwsClient2` options) if your plant installed real certs.
188
+ - **WebSocket subscription URL** comes from the `Location` header (real hardware) or the XHTML body (VC). Subprotocol: `rws_subscription` (RWS 2.0) / `robapi2_subscription` (RWS 1.0).
185
189
 
186
190
  ---
187
191
 
@@ -195,7 +199,10 @@ import { MultiRobotManager } from 'abb-rws-client';
195
199
  const multi = MultiRobotManager.fromConfigs([
196
200
  { id: 'cell-A', name: 'Cell A IRB120', host: '192.168.125.1', port: 80, useHttps: false, username: 'Admin', password: 'robotics' },
197
201
  { id: 'cell-B', name: 'Cell B IRB1200', host: '192.168.125.2', port: 443, useHttps: true, username: 'Admin', password: 'robotics' },
198
- ]);
202
+ ], {
203
+ refreshIntervalMs: 1000, // polling cadence (min 200); slow poll scales at 5×
204
+ strictTls: false, // true = verify controller TLS certificates
205
+ });
199
206
 
200
207
  multi.onError((msg, actions) => {
201
208
  console.error(`Robot error: ${msg}`);
@@ -214,17 +221,42 @@ multi.setActive('cell-B');
214
221
  ```
215
222
 
216
223
  `MultiRobotManager` wraps individual `RobotManager` instances. Each `RobotManager` handles its own:
217
- - Auto port discovery (probes 5466/9403/443/80/11811, plus a wide-scan fallback)
218
- - Protocol auto-detection (Digest vs Basic challenge)
219
- - 5-second polling for non-subscription state
220
- - WebSocket subscriptions with automatic polling fallback
221
- - Reconnect-on-failure (3-strike)
222
- - Clean `/logout` on disconnect (avoids orphan mastership locks)
224
+ - Auto port discovery (probes 5466 / 9403 / 443 / 80 / 11811 in that order, plus a wide-scan fallback when none of those answer — RobotStudio assigns random VC ports above 30000)
225
+ - Protocol auto-detection (`WWW-Authenticate: Digest` RWS 1.0; `Basic` → RWS 2.0)
226
+ - Hybrid polling cadence: **5× the refresh interval when WebSocket subscriptions are active** (positions only — state-change resources stream over WS); **the plain refresh interval when subscriptions are unavailable** (full state coverage via polling). Default 1 s / 5 s, configurable via `refreshIntervalMs`.
227
+ - WebSocket subscriptions on both protocols, with dropped-socket auto-reconnect and automatic degradation to fast polling if the event stream is terminally lost
228
+ - Reconnect-on-failure (3-strike, surfaces via `onError` listener)
229
+ - Clean `GET /logout` on disconnect releases server-side mastership and frees the session slot
223
230
 
224
231
  You can also create a `RobotManager` directly if you only have one robot.
225
232
 
226
233
  ---
227
234
 
235
+ ## `RobotManager` — higher-level surface
236
+
237
+ `RobotManager` wraps either client with operational helpers that handle mastership, polling, and protocol differences for you. In addition to delegating every protocol method to the underlying client, it exposes:
238
+
239
+ - **`getRmmpPrivilege()`**, **`requestRmmp(level)`** — Remote Mastership Privilege management. Required on OmniCore in AUTO mode for any modify op.
240
+ - **`getMastershipStatus(domain?)`** — read who currently holds rapid/cfg/motion mastership (uid + application name).
241
+ - **`setOperationMode('AUTO' | 'MANR' | 'MANF')`** — VC-only switch with auto-routing through MANR for AUTO ↔ MANF (the controller rejects direct transitions). Acquires `edit` mastership for the higher-privilege direction.
242
+ - **`setSpeedRatio(0..100)`** — wraps `edit` mastership; uses the live-verified `?action=setspeedratio&speed-ratio=N` form on RWS 2.0 (the bare endpoint returns 400).
243
+ - **`createBackup(name)`**, **`restoreBackup(name)`**, **`getBackupStatus()`**, **`listBackups()`** — `/ctrl/backup/...`
244
+ - **`callServiceRoutine(task, name, args?)`** — invoke a service routine remotely (calibration, brake check, etc.).
245
+ - **`calcJointsFromCartesian(...)`** — inverse kinematics. **`calcCartesianFromJoints(...)`** — forward kinematics.
246
+ - **`setActiveTool(mechunit, name)`**, **`setActiveWobj(mechunit, name)`** — switch active persistent tooldata / wobjdata.
247
+ - **CFG write** — `setCfgInstance` / `createCfgInstance` / `removeCfgInstance` / `loadCfgFile` / `saveCfgFile`, on **both** protocols (RWS 2.0 uses `instances/create-default` + the bracket value representation; RWS 1.0 uses the `?action=` forms — handled by the adapters). Each acquires the needed mastership for the duration.
248
+ - **DIPC** — `listDipcQueues` / `createDipcQueue` / `sendDipcMessage` / `readDipcMessage` / `removeDipcQueue`. Bidirectional messaging between RAPID and external clients.
249
+ - **`listFileVolumes()`** — every controller volume (HOME, BACKUP, DATA, ADDINDATA, PRODUCTS, RAMDISK, TEMP).
250
+ - **`getModuleSource(task, name)`** — pull a module's RAPID text in one call. Works even when the module has no backing file in `HOME` (loaded from `.pgf`/RobotStudio/pendant): the client saves it to the controller's TEMP volume, reads it, and deletes it.
251
+ - **`compressPath(source, dest)`** — controller-side compression.
252
+ - **`validateRapidValue(task, value, datatype)`** — pre-flight a literal before writing.
253
+ - **`RobotManager.discoverControllersMdns(opts?)`** *(static)* — find every ABB controller (real or virtual) announcing on the local network via mDNS/Bonjour. Returns system name, host, RWS port, RobotWare version, system GUID, and a `'rws1' | 'rws2'` classification — no port scanning needed.
254
+ - **Simulation panel** *(RWS 2.0 virtual controllers only)* — `simEmergencyStop()`, `simResetEmergencyStop()`, `simGeneralStop(engage?)`, `simAutoStop(engage?)`, `simEnableSwitch(on)`, `teleportMechunit(mechunit, joints, extJoints?)`. Drive the E-stop / guard-stop chain and reposition the simulated robot from the API. Throw `RwsError` on real hardware and RW6 (VC-only endpoints).
255
+
256
+ Every method returns `Promise<...>` and throws `RwsError` on failure.
257
+
258
+ ---
259
+
228
260
  ## Logging
229
261
 
230
262
  The lib ships a no-op logger by default. Hosts (CLIs, services, the VS Code extension) install their own:
@@ -460,7 +492,7 @@ try {
460
492
 
461
493
  ## Session Persistence
462
494
 
463
- The IRC5 controller allows a maximum of **70 concurrent RWS sessions**. Persist the session cookie across restarts to always reuse the same slot:
495
+ IRC5 controllers allow a maximum of **70 concurrent RWS sessions**; OmniCore controllers also have a finite session pool (the exact number isn't published, but heavy probing without `/logout` returns HTTP 503 once it fills). Persist the session cookie across restarts to always reuse the same slot — the lib calls `/logout` on `disconnect()` to free the slot cleanly, but a saved cookie is the most robust path:
464
496
 
465
497
  ```ts
466
498
  import fs from 'fs';
@@ -481,14 +513,16 @@ await client.connect(); // reuses the existing session slot
481
513
 
482
514
  ## Rate Limits
483
515
 
484
- | Constraint | Value |
485
- |------------|-------|
486
- | Max request rate | 20 req/sec |
487
- | Client enforced interval | 55 ms between requests |
488
- | Max concurrent sessions | 70 |
489
- | Session inactivity timeout | 5 minutes |
490
- | Max session lifetime | 25 minutes |
491
- | Max file upload | 800 MB |
516
+ | Constraint | Value | Source |
517
+ |------------|-------|--------|
518
+ | Max request rate | 20 req/sec | Both protocols (controller-enforced) |
519
+ | Client-enforced interval | 55 ms between requests | This package, default |
520
+ | Max concurrent sessions | 70 (IRC5) / finite (OmniCore) | Controller-enforced |
521
+ | Session inactivity timeout | 5 minutes (IRC5) | Controller-enforced |
522
+ | Max session lifetime | 25 minutes (IRC5) | Controller-enforced |
523
+ | Max file upload | 800 MB (IRC5) | Controller-enforced; OmniCore is similar |
524
+
525
+ OmniCore-specific limits aren't fully documented by ABB; what the lib observes empirically matches the IRC5 numbers within an order of magnitude.
492
526
 
493
527
  ---
494
528
 
@@ -496,10 +530,13 @@ await client.connect(); // reuses the existing session slot
496
530
 
497
531
  | | RobotWare 6.x (RWS 1.0) | RobotWare 7.x (RWS 2.0) |
498
532
  |--|--|--|
499
- | This package | ✅ Supported | Not compatible |
500
- | IRC5 controller | ✅ | n/a |
501
- | OmniCore controller | n/a | |
502
- | RobotStudio virtual | ✅ (127.0.0.1) | |
533
+ | This package | ✅ `RwsClient` | `RwsClient2` |
534
+ | IRC5 controller (real) | ✅ | n/a |
535
+ | OmniCore controller (real) | n/a | |
536
+ | RobotStudio virtual controller | ✅ (RW6.x VC) | (RW7.x VC) |
537
+ | Auto-detect from one entry point | ✅ via `createClient()` | ✅ via `createClient()` |
538
+
539
+ **Live-tested matrix** as of v0.7.1: RobotWare 7.21 (OmniCore VC, RWS 2.0), RobotWare 6.16 (IRC5 VC, RWS 1.0). 116 unit tests + 339 live protocol-coverage tests pass against both.
503
540
 
504
541
  ---
505
542
 
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Parser for RWS 2.0 HAL JSON responses (`application/hal+json;v=2.0`) — the
3
+ * officially supported primary GET representation on OmniCore controllers.
4
+ *
5
+ * Live-verified 2026-07-09 on OmniCore VC RW7.21 across every GET endpoint
6
+ * family the client touches (panel, rapid, motionsystem, system, ctrl, elog,
7
+ * iosystem, cfg, mastership, users/rmmp). The wire shape is:
8
+ *
9
+ * { "_links": { "base": {...}, "self": {...} [, "next": {...}] },
10
+ * "status": { "code": 294912 }, // negative on errors, plus "msg"
11
+ * "state": [ { "_type": "...", "_title": "...", ...fields } ],
12
+ * "_embedded": { "resources": [ { "_type": "...", ...fields } ] } }
13
+ *
14
+ * `_type` carries the same identifier the XHTML representation puts in the
15
+ * `<li class="...">` attribute, `_title` mirrors the `title` attribute, and
16
+ * the former `<span class="X">` fields become plain JSON keys. Resources can
17
+ * nest (e.g. `sys-options-li` holds an `options` array of typed objects,
18
+ * `cfg-dt-instance-li` an `attrib` array of `cfg-ia-t` entries), so lookups
19
+ * recurse — mirroring how the XHTML parser finds `<li>` anywhere in the
20
+ * document. Presents the same read interface as `XhtmlParser` so `RwsClient2`
21
+ * can treat both representations alike.
22
+ */
23
+ export declare class HalJsonParser {
24
+ private readonly root;
25
+ constructor(body: string);
26
+ /** Cheap sniff used to pick a parser: HAL bodies are single JSON objects. */
27
+ static looksLikeJson(body: string): boolean;
28
+ /** Returns the field map of the first resource whose `_type` matches. */
29
+ getState(type: string): Record<string, string>;
30
+ /** Returns field maps for every resource with a matching `_type`, in document order. */
31
+ getAllStates(type: string): Array<Record<string, string>>;
32
+ /** First scalar value stored under `key` in any resource (XhtmlParser.get analogue). */
33
+ get(key: string): string | undefined;
34
+ /**
35
+ * Error details from the top-level status block. Success responses carry a
36
+ * positive code (294912 observed live); errors a negative code plus `msg`.
37
+ */
38
+ getError(): {
39
+ code: string;
40
+ msg: string;
41
+ } | null;
42
+ /**
43
+ * Pagination link (`_links.next.href`), raw as sent. Live-verified quirk:
44
+ * the controller XML-escapes ampersands even inside JSON strings
45
+ * (`"signals?start=3&amp;limit=3"`) — callers must unescape, exactly as on
46
+ * the XHTML `rel="next"` path.
47
+ */
48
+ nextHref(): string | undefined;
49
+ private rootObject;
50
+ /** Depth-first visit of every object in the tree, skipping `_links` blocks. */
51
+ private walk;
52
+ /** Scalar fields → strings; `_title` kept; self link exposed as `_href` (XHTML parity). */
53
+ private static flatten;
54
+ }
55
+ //# sourceMappingURL=HalJsonParser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HalJsonParser.d.ts","sourceRoot":"","sources":["../src/HalJsonParser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAkB;gBAE3B,IAAI,EAAE,MAAM;IAMxB,6EAA6E;IAC7E,MAAM,CAAC,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI3C,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAI9C,wFAAwF;IACxF,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAQzD,wFAAwF;IACxF,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAYpC;;;OAGG;IACH,QAAQ,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAShD;;;;;OAKG;IACH,QAAQ,IAAI,MAAM,GAAG,SAAS;IAS9B,OAAO,CAAC,UAAU;IAMlB,+EAA+E;IAC/E,OAAO,CAAC,IAAI;IAcZ,2FAA2F;IAC3F,OAAO,CAAC,MAAM,CAAC,OAAO;CAmBvB"}
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Parser for RWS 2.0 HAL JSON responses (`application/hal+json;v=2.0`) — the
3
+ * officially supported primary GET representation on OmniCore controllers.
4
+ *
5
+ * Live-verified 2026-07-09 on OmniCore VC RW7.21 across every GET endpoint
6
+ * family the client touches (panel, rapid, motionsystem, system, ctrl, elog,
7
+ * iosystem, cfg, mastership, users/rmmp). The wire shape is:
8
+ *
9
+ * { "_links": { "base": {...}, "self": {...} [, "next": {...}] },
10
+ * "status": { "code": 294912 }, // negative on errors, plus "msg"
11
+ * "state": [ { "_type": "...", "_title": "...", ...fields } ],
12
+ * "_embedded": { "resources": [ { "_type": "...", ...fields } ] } }
13
+ *
14
+ * `_type` carries the same identifier the XHTML representation puts in the
15
+ * `<li class="...">` attribute, `_title` mirrors the `title` attribute, and
16
+ * the former `<span class="X">` fields become plain JSON keys. Resources can
17
+ * nest (e.g. `sys-options-li` holds an `options` array of typed objects,
18
+ * `cfg-dt-instance-li` an `attrib` array of `cfg-ia-t` entries), so lookups
19
+ * recurse — mirroring how the XHTML parser finds `<li>` anywhere in the
20
+ * document. Presents the same read interface as `XhtmlParser` so `RwsClient2`
21
+ * can treat both representations alike.
22
+ */
23
+ export class HalJsonParser {
24
+ root;
25
+ constructor(body) {
26
+ let parsed = null;
27
+ try {
28
+ parsed = JSON.parse(body);
29
+ }
30
+ catch {
31
+ parsed = null;
32
+ }
33
+ this.root = parsed;
34
+ }
35
+ /** Cheap sniff used to pick a parser: HAL bodies are single JSON objects. */
36
+ static looksLikeJson(body) {
37
+ return body.trimStart().startsWith('{');
38
+ }
39
+ /** Returns the field map of the first resource whose `_type` matches. */
40
+ getState(type) {
41
+ return this.getAllStates(type)[0] ?? {};
42
+ }
43
+ /** Returns field maps for every resource with a matching `_type`, in document order. */
44
+ getAllStates(type) {
45
+ const results = [];
46
+ this.walk(this.root, obj => {
47
+ if (obj['_type'] === type) {
48
+ results.push(HalJsonParser.flatten(obj));
49
+ }
50
+ });
51
+ return results;
52
+ }
53
+ /** First scalar value stored under `key` in any resource (XhtmlParser.get analogue). */
54
+ get(key) {
55
+ let found;
56
+ this.walk(this.root, obj => {
57
+ if (found !== undefined) {
58
+ return;
59
+ }
60
+ const v = obj[key];
61
+ if (v !== undefined && v !== null && typeof v !== 'object' && !key.startsWith('_')) {
62
+ found = String(v);
63
+ }
64
+ });
65
+ return found;
66
+ }
67
+ /**
68
+ * Error details from the top-level status block. Success responses carry a
69
+ * positive code (294912 observed live); errors a negative code plus `msg`.
70
+ */
71
+ getError() {
72
+ const status = this.rootObject()?.['status'];
73
+ if (!status || typeof status !== 'object' || Array.isArray(status)) {
74
+ return null;
75
+ }
76
+ const code = status['code'];
77
+ if (typeof code !== 'number' || code >= 0) {
78
+ return null;
79
+ }
80
+ const msg = status['msg'];
81
+ return { code: String(code), msg: typeof msg === 'string' ? msg : '' };
82
+ }
83
+ /**
84
+ * Pagination link (`_links.next.href`), raw as sent. Live-verified quirk:
85
+ * the controller XML-escapes ampersands even inside JSON strings
86
+ * (`"signals?start=3&amp;limit=3"`) — callers must unescape, exactly as on
87
+ * the XHTML `rel="next"` path.
88
+ */
89
+ nextHref() {
90
+ const links = this.rootObject()?.['_links'];
91
+ if (!links || typeof links !== 'object' || Array.isArray(links)) {
92
+ return undefined;
93
+ }
94
+ const next = links['next'];
95
+ if (!next || typeof next !== 'object' || Array.isArray(next)) {
96
+ return undefined;
97
+ }
98
+ const href = next['href'];
99
+ return typeof href === 'string' && href ? href : undefined;
100
+ }
101
+ rootObject() {
102
+ return this.root && typeof this.root === 'object' && !Array.isArray(this.root)
103
+ ? this.root
104
+ : null;
105
+ }
106
+ /** Depth-first visit of every object in the tree, skipping `_links` blocks. */
107
+ walk(node, visit) {
108
+ if (Array.isArray(node)) {
109
+ for (const item of node) {
110
+ this.walk(item, visit);
111
+ }
112
+ return;
113
+ }
114
+ if (node && typeof node === 'object') {
115
+ visit(node);
116
+ for (const [key, value] of Object.entries(node)) {
117
+ if (key === '_links') {
118
+ continue;
119
+ }
120
+ if (value && typeof value === 'object') {
121
+ this.walk(value, visit);
122
+ }
123
+ }
124
+ }
125
+ }
126
+ /** Scalar fields → strings; `_title` kept; self link exposed as `_href` (XHTML parity). */
127
+ static flatten(obj) {
128
+ const fields = {};
129
+ const title = obj['_title'];
130
+ if (typeof title === 'string') {
131
+ fields['_title'] = title;
132
+ }
133
+ const links = obj['_links'];
134
+ if (links && typeof links === 'object' && !Array.isArray(links)) {
135
+ const self = links['self'];
136
+ if (self && typeof self === 'object' && !Array.isArray(self)) {
137
+ const href = self['href'];
138
+ if (typeof href === 'string') {
139
+ fields['_href'] = href;
140
+ }
141
+ }
142
+ }
143
+ for (const [key, value] of Object.entries(obj)) {
144
+ if (key.startsWith('_')) {
145
+ continue;
146
+ }
147
+ if (value === null || typeof value === 'object') {
148
+ continue;
149
+ } // nested resources have their own _type
150
+ fields[key] = String(value);
151
+ }
152
+ return fields;
153
+ }
154
+ }
155
+ //# sourceMappingURL=HalJsonParser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HalJsonParser.js","sourceRoot":"","sources":["../src/HalJsonParser.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"HttpSession.d.ts","sourceRoot":"","sources":["../src/HttpSession.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAmB,YAAY,EAAE,MAAM,YAAY,CAAC;AAchE,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAID,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAEnC,2DAA2D;IAC3D,OAAO,CAAC,OAAO,CAAkC;IAEjD,yDAAyD;IACzD,OAAO,CAAC,eAAe,CAAgC;IAEvD,sEAAsE;IACtE,OAAO,CAAC,UAAU,CAAK;IAEvB,gDAAgD;IAChD,OAAO,CAAC,eAAe,CAAK;IAE5B,iFAAiF;IACjF,OAAO,CAAC,gBAAgB,CAAK;IAE7B,0DAA0D;IAC1D,OAAO,CAAC,YAAY,CAAoC;IAExD,yDAAyD;IACzD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAiB;gBAE/C,OAAO,EAAE,kBAAkB;IAqBvC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAIxC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAIxD,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC;IAInE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAI3C,yEAAyE;IACzE,eAAe,IAAI,MAAM;IAIzB,yFAAyF;IACzF,gBAAgB,IAAI,MAAM,GAAG,IAAI;IAKjC;;;;;;;;OAQG;IACH,YAAY,IAAI,IAAI;IAMpB;;;;OAIG;IACH,OAAO,CAAC,OAAO;YAsBD,OAAO;IA4ErB,iFAAiF;YACnE,QAAQ;IAoDtB;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IA8B5B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,eAAe;IAwCvB;;;;;OAKG;IACH,OAAO,CAAC,YAAY;IAyBpB,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,aAAa;CAetB"}
1
+ {"version":3,"file":"HttpSession.d.ts","sourceRoot":"","sources":["../src/HttpSession.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAmB,YAAY,EAAE,MAAM,YAAY,CAAC;AAchE,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAID,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAEnC,2DAA2D;IAC3D,OAAO,CAAC,OAAO,CAAkC;IAEjD,yDAAyD;IACzD,OAAO,CAAC,eAAe,CAAgC;IAEvD,sEAAsE;IACtE,OAAO,CAAC,UAAU,CAAK;IAEvB,gDAAgD;IAChD,OAAO,CAAC,eAAe,CAAK;IAE5B,iFAAiF;IACjF,OAAO,CAAC,gBAAgB,CAAK;IAE7B,0DAA0D;IAC1D,OAAO,CAAC,YAAY,CAAoC;IAExD,yDAAyD;IACzD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAiB;gBAE/C,OAAO,EAAE,kBAAkB;IAqBvC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAIxC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAIxD,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC;IAInE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAI3C,yEAAyE;IACzE,eAAe,IAAI,MAAM;IAIzB,yFAAyF;IACzF,gBAAgB,IAAI,MAAM,GAAG,IAAI;IAKjC;;;;;;;;OAQG;IACH,YAAY,IAAI,IAAI;IAMpB;;;;OAIG;IACH,OAAO,CAAC,OAAO;YAsBD,OAAO;IA4ErB,iFAAiF;YACnE,QAAQ;IAoDtB;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IA8B5B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,eAAe;IAwDvB;;;;;OAKG;IACH,OAAO,CAAC,YAAY;IAyBpB,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,aAAa;CAetB"}
@@ -267,12 +267,23 @@ export class HttpSession {
267
267
  */
268
268
  buildAuthHeader(method, uri) {
269
269
  const challenge = this.digestChallenge;
270
+ // qop may be a comma-separated list, e.g. qop="auth,auth-int". Pick auth when
271
+ // offered; auth-int (body-hash mode) is not implemented, so an auth-int-only
272
+ // challenge must fail loudly rather than answer with a plain-auth hash.
273
+ let useQopAuth = false;
274
+ if (challenge.qop) {
275
+ const offered = challenge.qop.split(',').map((q) => q.trim());
276
+ if (!offered.includes('auth')) {
277
+ throw new RwsError(`Digest qop "${challenge.qop}" is not supported — only qop=auth`, 'AUTH_FAILED');
278
+ }
279
+ useQopAuth = true;
280
+ }
270
281
  const nc = (++this.nonceCount).toString(16).padStart(8, '0');
271
282
  const cnonce = randomBytes(16).toString('hex');
272
283
  const ha1 = md5(`${this.username}:${challenge.realm}:${this.password}`);
273
284
  const ha2 = md5(`${method}:${uri}`);
274
285
  let responseHash;
275
- if (challenge.qop === 'auth' || challenge.qop === 'auth-int') {
286
+ if (useQopAuth) {
276
287
  // RFC 2617 qop mode
277
288
  responseHash = md5(`${ha1}:${challenge.nonce}:${nc}:${cnonce}:auth:${ha2}`);
278
289
  }
@@ -290,7 +301,7 @@ export class HttpSession {
290
301
  `cnonce="${cnonce}"`,
291
302
  `response="${responseHash}"`,
292
303
  ];
293
- if (challenge.qop) {
304
+ if (useQopAuth) {
294
305
  parts.push(`qop=auth`);
295
306
  }
296
307
  if (challenge.opaque) {