abb-rws-client 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Meraj Safari
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,261 @@
1
+ # abb-rws-client
2
+
3
+ A typed TypeScript/Node.js HTTP and WebSocket client for **ABB IRC5 robot controllers** using [Robot Web Services (RWS) 1.0](https://developercenter.robotstudio.com/api/rwsApi/).
4
+
5
+ > **Compatibility:** RWS 1.0 / RobotWare 6.x only.
6
+ > **Not compatible** with OmniCore controllers, RobotWare 7.x, or RWS 2.0.
7
+
8
+ ---
9
+
10
+ ## Features
11
+
12
+ - HTTP Digest Authentication (RFC 2617) — no external auth libraries
13
+ - Session cookie management (`ABBCX`, `-http-session-`)
14
+ - Request rate limiting (< 20 req/sec, configurable)
15
+ - Automatic session re-authentication after 5-minute inactivity
16
+ - WebSocket subscriptions for real-time events (execution state, I/O signals, etc.)
17
+ - Auto-reconnect on WebSocket disconnect (3 retries, exponential backoff)
18
+ - Fully typed public API — every method throws `RwsError` with a typed `code`
19
+ - Zero runtime dependencies — Node 18+ built-ins only
20
+
21
+ ---
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install abb-rws-client
27
+ ```
28
+
29
+ **Requirements:** Node.js 21+ (or Node 18 with `--experimental-websocket` for WebSocket subscription support).
30
+
31
+ ---
32
+
33
+ ## Quick Start — RobotStudio virtual controller
34
+
35
+ ```ts
36
+ import { RwsClient, RwsError } from 'abb-rws-client';
37
+
38
+ const client = new RwsClient({
39
+ host: '127.0.0.1', // RobotStudio default
40
+ port: 80,
41
+ username: 'Default User',
42
+ password: 'robotics',
43
+ });
44
+
45
+ try {
46
+ await client.connect();
47
+
48
+ // Read state
49
+ const state = await client.getControllerState();
50
+ console.log('Controller state:', state); // e.g. 'motoron'
51
+
52
+ const mode = await client.getOperationMode();
53
+ console.log('Operation mode:', mode); // e.g. 'AUTO'
54
+
55
+ // Read positions
56
+ const joints = await client.getJointPositions();
57
+ console.log('J1:', joints.rax_1, 'degrees');
58
+
59
+ const tcp = await client.getCartesianPosition();
60
+ console.log('TCP:', tcp.x, tcp.y, tcp.z, 'mm');
61
+
62
+ // RAPID execution
63
+ await client.startRapid();
64
+ await client.stopRapid();
65
+
66
+ // I/O signals
67
+ const di = await client.readSignal('Local', 'DRV_1', 'DI_1');
68
+ console.log('DI_1 =', di.value);
69
+ await client.writeSignal('Local', 'DRV_1', 'DO_1', '1');
70
+
71
+ // Upload and load a RAPID module
72
+ const modSource = `MODULE MyMod\n PROC main()\n TPWrite "Hello";\n ENDPROC\nENDMODULE`;
73
+ await client.uploadModule('$HOME/MyMod.mod', modSource);
74
+ await client.loadModule('T_ROB1', '$HOME/MyMod.mod');
75
+
76
+ // Subscribe to real-time events
77
+ const unsubscribe = await client.subscribe(
78
+ ['execution', 'controllerstate'],
79
+ (event) => {
80
+ console.log(`[${event.timestamp.toISOString()}] ${event.resource} = ${event.value}`);
81
+ }
82
+ );
83
+
84
+ // ... later, unsubscribe and disconnect
85
+ await unsubscribe();
86
+ await client.disconnect();
87
+
88
+ } catch (e) {
89
+ if (e instanceof RwsError) {
90
+ console.error(`[${e.code}] ${e.message} (HTTP ${e.httpStatus ?? 'N/A'})`);
91
+ } else {
92
+ throw e;
93
+ }
94
+ }
95
+ ```
96
+
97
+ ---
98
+
99
+ ## API Reference
100
+
101
+ ### `new RwsClient(options)`
102
+
103
+ | Option | Type | Default | Description |
104
+ |---|---|---|---|
105
+ | `host` | `string` | — | Controller IP or hostname |
106
+ | `port` | `number` | `80` | HTTP port |
107
+ | `username` | `string` | `'Default User'` | RWS username |
108
+ | `password` | `string` | `'robotics'` | RWS password |
109
+ | `requestIntervalMs` | `number` | `55` | Minimum ms between requests (enforces < 20 req/sec) |
110
+ | `timeout` | `number` | `5000` | Request timeout in ms |
111
+
112
+ ---
113
+
114
+ ### Connection
115
+
116
+ | Method | Returns | Description |
117
+ |---|---|---|
118
+ | `connect()` | `Promise<void>` | Establish session and authenticate |
119
+ | `disconnect()` | `Promise<void>` | Close subscriptions and clear session |
120
+
121
+ ---
122
+
123
+ ### Controller State
124
+
125
+ | Method | Returns | Description |
126
+ |---|---|---|
127
+ | `getControllerState()` | `Promise<ControllerState>` | `'init'` \| `'motoroff'` \| `'motoron'` \| `'guardstop'` \| `'emergencystop'` \| `'emergencystopreset'` \| `'sysfail'` |
128
+ | `getOperationMode()` | `Promise<OperationMode>` | `'AUTO'` \| `'MANR'` \| `'MANF'` |
129
+
130
+ ---
131
+
132
+ ### RAPID Execution
133
+
134
+ | Method | Returns | Description |
135
+ |---|---|---|
136
+ | `getRapidExecutionState()` | `Promise<ExecutionState>` | `'running'` \| `'stopped'` |
137
+ | `getRapidTasks()` | `Promise<RapidTask[]>` | All RAPID tasks and their states |
138
+ | `startRapid()` | `Promise<void>` | Start RAPID execution (requires AUTO + motors on) |
139
+ | `stopRapid()` | `Promise<void>` | Stop RAPID execution |
140
+ | `resetRapid()` | `Promise<void>` | Reset program pointer to main |
141
+
142
+ ---
143
+
144
+ ### Motion
145
+
146
+ | Method | Returns | Description |
147
+ |---|---|---|
148
+ | `getJointPositions(mechunit?)` | `Promise<JointTarget>` | Joint angles in degrees for all 6 axes |
149
+ | `getCartesianPosition(mechunit?, tool?, wobj?)` | `Promise<RobTarget>` | TCP position (mm) and orientation (quaternion) |
150
+
151
+ ---
152
+
153
+ ### Modules
154
+
155
+ | Method | Returns | Description |
156
+ |---|---|---|
157
+ | `uploadModule(remotePath, content)` | `Promise<void>` | Upload RAPID `.mod` source to controller filesystem |
158
+ | `loadModule(taskName, modulePath)` | `Promise<void>` | Load an uploaded module into a RAPID task |
159
+ | `listModules(taskName)` | `Promise<string[]>` | Names of all loaded modules in a task |
160
+
161
+ ---
162
+
163
+ ### I/O Signals
164
+
165
+ | Method | Returns | Description |
166
+ |---|---|---|
167
+ | `readSignal(network, device, name)` | `Promise<Signal>` | Read current signal value |
168
+ | `writeSignal(network, device, name, value)` | `Promise<void>` | Write a value to an output signal |
169
+
170
+ ---
171
+
172
+ ### Subscriptions
173
+
174
+ ```ts
175
+ subscribe(
176
+ resources: SubscriptionResource[],
177
+ handler: (event: SubscriptionEvent) => void
178
+ ): Promise<() => Promise<void>>
179
+ ```
180
+
181
+ Subscribe to real-time RWS events. Returns an async unsubscribe function.
182
+
183
+ **`SubscriptionResource`** can be:
184
+ - `'execution'` — RAPID execution state changes
185
+ - `'controllerstate'` — controller state changes
186
+ - `'operationmode'` — operation mode changes
187
+ - `{ type: 'signal'; name: 'network/device/signalname' }` — I/O signal changes
188
+ - `{ type: 'persvar'; name: 'task/varname' }` — RAPID persistent variable changes
189
+
190
+ **`SubscriptionEvent`**:
191
+ ```ts
192
+ {
193
+ resource: string; // RWS path of the changed resource
194
+ value: string; // New value as string
195
+ timestamp: Date; // When the event was received
196
+ }
197
+ ```
198
+
199
+ ---
200
+
201
+ ### Error Handling
202
+
203
+ All public methods throw `RwsError` (never plain `Error`) with a typed `code`:
204
+
205
+ | Code | Meaning |
206
+ |---|---|
207
+ | `'AUTH_FAILED'` | Wrong credentials or session rejected |
208
+ | `'SESSION_EXPIRED'` | Session timed out and re-auth was not possible |
209
+ | `'MOTORS_OFF'` | Action requires motors to be on |
210
+ | `'MODULE_NOT_FOUND'` | Module file not found on controller filesystem |
211
+ | `'CONTROLLER_BUSY'` | Controller returned 503; retry later |
212
+ | `'RATE_LIMITED'` | Too many requests (429) |
213
+ | `'NETWORK_ERROR'` | TCP/timeout/WebSocket error |
214
+ | `'PARSE_ERROR'` | Unexpected XML response format |
215
+ | `'UNKNOWN'` | Unmapped error; check `httpStatus` and `rwsDetail` |
216
+
217
+ ```ts
218
+ try {
219
+ await client.startRapid();
220
+ } catch (e) {
221
+ if (e instanceof RwsError) {
222
+ switch (e.code) {
223
+ case 'MOTORS_OFF':
224
+ console.error('Enable motors on the FlexPendant first');
225
+ break;
226
+ case 'AUTH_FAILED':
227
+ console.error('Check username/password');
228
+ break;
229
+ default:
230
+ console.error(`Unexpected error [${e.code}]: ${e.message}`);
231
+ }
232
+ }
233
+ }
234
+ ```
235
+
236
+ ---
237
+
238
+ ## Compatibility
239
+
240
+ | Feature | RobotWare 6.x (RWS 1.0) | RobotWare 7.x (RWS 2.0) |
241
+ |---|---|---|
242
+ | This package | ✓ Supported | ✗ Not compatible |
243
+ | Controller: IRC5 | ✓ | n/a |
244
+ | Controller: OmniCore | n/a | ✗ |
245
+ | RobotStudio (virtual) | ✓ (127.0.0.1) | ✗ |
246
+
247
+ RWS 2.0 uses a completely different API structure. This package implements RWS 1.0 only and will not function with RobotWare 7.x or OmniCore controllers.
248
+
249
+ ---
250
+
251
+ ## Resources
252
+
253
+ - [ABB RWS 1.0 API Reference](https://developercenter.robotstudio.com/api/rwsApi/)
254
+ - [ABB Developer Center](https://developercenter.robotstudio.com/api/RWS)
255
+ - [VALIDATION.md](./VALIDATION.md) — Manual test procedures against a real IRC5 / RobotStudio controller
256
+
257
+ ---
258
+
259
+ ## License
260
+
261
+ MIT — see [LICENSE](./LICENSE)
@@ -0,0 +1,92 @@
1
+ /**
2
+ * HttpSession — HTTP communication layer for ABB IRC5 controllers.
3
+ *
4
+ * Features:
5
+ * - HTTP Digest Authentication (RFC 2617) implemented from scratch using node:crypto
6
+ * - ABBCX + -http-session- cookie management
7
+ * - Request queue enforcing minimum interval between requests (<20 req/sec RWS limit)
8
+ * - Automatic re-authentication on session expiry (5-minute inactivity)
9
+ * - 401 retry with fresh digest handshake; 503 retry with 200ms backoff
10
+ * - AbortController-based timeout
11
+ *
12
+ * Uses Node 18+ built-in fetch and node:crypto. Zero external dependencies.
13
+ */
14
+ import type { HttpResponse } from './types.js';
15
+ export interface HttpSessionOptions {
16
+ baseUrl: string;
17
+ username: string;
18
+ password: string;
19
+ requestIntervalMs: number;
20
+ timeoutMs: number;
21
+ }
22
+ export declare class HttpSession {
23
+ private readonly baseUrl;
24
+ private readonly username;
25
+ private readonly password;
26
+ private readonly requestIntervalMs;
27
+ private readonly timeoutMs;
28
+ /** Stored session cookies: '-http-session-' and 'ABBCX' */
29
+ private cookies;
30
+ /** Last parsed digest challenge from WWW-Authenticate */
31
+ private digestChallenge;
32
+ /** Nonce use counter — reset to 0 whenever a new nonce is received */
33
+ private nonceCount;
34
+ /** Timestamp of the most recent request sent */
35
+ private lastRequestTime;
36
+ /** Timestamp of the most recent successful response — used for session expiry */
37
+ private lastActivityTime;
38
+ /** Promise chain that serialises all outbound requests */
39
+ private requestQueue;
40
+ /** 5-minute session inactivity timeout (milliseconds) */
41
+ private static readonly SESSION_TIMEOUT_MS;
42
+ constructor(options: HttpSessionOptions);
43
+ get(path: string): Promise<HttpResponse>;
44
+ post(path: string, body?: string): Promise<HttpResponse>;
45
+ put(path: string, body: string | Uint8Array): Promise<HttpResponse>;
46
+ delete(path: string): Promise<HttpResponse>;
47
+ /** Returns the current cookie string for use in WebSocket connections */
48
+ getCookieHeader(): string;
49
+ /** Clear all session state (called on disconnect) */
50
+ clearSession(): void;
51
+ /**
52
+ * Append a function to the serial request queue, enforcing the minimum interval
53
+ * between requests. Error suppression on `this.requestQueue` (not on `result`)
54
+ * ensures queue continues processing even when individual requests fail.
55
+ */
56
+ private enqueue;
57
+ private execute;
58
+ /** Issue a single HTTP request with digest auth header if we have a challenge */
59
+ private rawFetch;
60
+ /**
61
+ * Parse the WWW-Authenticate: Digest ... header into a DigestChallenge.
62
+ * Handles both quoted and unquoted parameter values per RFC 2617.
63
+ */
64
+ private parseDigestChallenge;
65
+ /**
66
+ * Build the Authorization: Digest ... header value for the given request.
67
+ * Increments the nonce use counter (nc).
68
+ *
69
+ * RFC 2617 §3.2.2:
70
+ * HA1 = MD5(username:realm:password)
71
+ * HA2 = MD5(method:digestURI)
72
+ * response = MD5(HA1:nonce:nc:cnonce:qop:HA2) — when qop=auth
73
+ * response = MD5(HA1:nonce:HA2) — RFC 2069 compat (no qop)
74
+ *
75
+ * Important: the space in 'Default User' is NOT percent-encoded for HA1.
76
+ * The nc value is NOT quoted in the Authorization header.
77
+ * The URI is path+query only, not scheme://host:port/path.
78
+ */
79
+ private buildAuthHeader;
80
+ /**
81
+ * Extract Set-Cookie headers from a response and store name=value pairs.
82
+ * Uses Headers.getSetCookie() (Node 18.14.1+) to correctly handle multiple
83
+ * Set-Cookie headers. Falls back to headers.get('set-cookie') on older Node 18,
84
+ * though this may misparse cookie values containing commas.
85
+ */
86
+ private storeCookies;
87
+ private buildCookieHeader;
88
+ private isSessionExpired;
89
+ private isOk;
90
+ private mapHttpStatus;
91
+ }
92
+ //# sourceMappingURL=HttpSession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HttpSession.d.ts","sourceRoot":"","sources":["../src/HttpSession.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,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;CACnB;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;IAUvC,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,qDAAqD;IACrD,YAAY,IAAI,IAAI;IASpB;;;;OAIG;IACH,OAAO,CAAC,OAAO;YAsBD,OAAO;IA0DrB,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"}