abb-rws-client 0.1.9 → 0.5.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/README.md CHANGED
@@ -3,20 +3,28 @@
3
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
4
 
5
5
  > **Compatibility:** RWS 1.0 / RobotWare 6.x only.
6
- > **Not compatible** with OmniCore controllers, RobotWare 7.x, or RWS 2.0.
6
+ > Not compatible with OmniCore, RobotWare 7.x, or RWS 2.0.
7
+
8
+ ---
9
+
10
+ ## VS Code Extension
11
+
12
+ Prefer a GUI? The companion VS Code extension gives you live status, motion data, RAPID control, I/O signals, event log, and file management directly from the sidebar — no code required.
13
+
14
+ **[ABB Robot (RWS) — VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=merajsafari.abb-rws)**
7
15
 
8
16
  ---
9
17
 
10
18
  ## Features
11
19
 
12
20
  - HTTP Digest Authentication (RFC 2617) — no external auth libraries
13
- - Session cookie management (`ABBCX`, `-http-session-`)
21
+ - Session cookie management (`ABBCX`, `-http-session-`) with automatic reuse
14
22
  - 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.)
23
+ - Automatic re-authentication after session expiry
24
+ - WebSocket subscriptions for real-time events
17
25
  - Auto-reconnect on WebSocket disconnect (3 retries, exponential backoff)
18
26
  - Fully typed public API — every method throws `RwsError` with a typed `code`
19
- - Zero runtime dependencies — Node 18+ built-ins only
27
+ - Zero runtime dependencies — Node.js built-ins only
20
28
 
21
29
  ---
22
30
 
@@ -26,233 +34,320 @@ A typed TypeScript/Node.js HTTP and WebSocket client for **ABB IRC5 robot contro
26
34
  npm install abb-rws-client
27
35
  ```
28
36
 
29
- **Requirements:** Node.js 21+ (or Node 18 with `--experimental-websocket` for WebSocket subscription support).
37
+ **Requirements:** Node.js 18+ (WebSocket subscriptions require Node 21+ or Node 18 with `--experimental-websocket`).
30
38
 
31
39
  ---
32
40
 
33
- ## Quick Start — RobotStudio virtual controller
41
+ ## Quick Start
34
42
 
35
43
  ```ts
36
44
  import { RwsClient, RwsError } from 'abb-rws-client';
37
45
 
38
46
  const client = new RwsClient({
39
- host: '127.0.0.1', // RobotStudio default
40
- port: 80,
47
+ host: '192.168.125.1',
41
48
  username: 'Default User',
42
49
  password: 'robotics',
43
50
  });
44
51
 
45
- try {
46
- await client.connect();
52
+ await client.connect();
47
53
 
48
- // Read state
49
- const state = await client.getControllerState();
50
- console.log('Controller state:', state); // e.g. 'motoron'
54
+ // Controller state
55
+ const state = await client.getControllerState(); // 'motoron' | 'motoroff' | ...
56
+ const mode = await client.getOperationMode(); // 'AUTO' | 'MANR' | 'MANF'
51
57
 
52
- const mode = await client.getOperationMode();
53
- console.log('Operation mode:', mode); // e.g. 'AUTO'
58
+ // Motion
59
+ const joints = await client.getJointPositions(); // rax_1..rax_6 in degrees
60
+ const tcp = await client.getCartesianPosition(); // x/y/z mm + quaternion
61
+ const tcpConfig = await client.getCartesianFull(); // + j1/j4/j6/jx config flags
54
62
 
55
- // Read positions
56
- const joints = await client.getJointPositions();
57
- console.log('J1:', joints.rax_1, 'degrees');
63
+ // RAPID
64
+ await client.startRapid();
65
+ await client.stopRapid();
66
+ await client.resetRapid(); // PP to Main
58
67
 
59
- const tcp = await client.getCartesianPosition();
60
- console.log('TCP:', tcp.x, tcp.y, tcp.z, 'mm');
68
+ // Variables
69
+ const val = await client.getRapidVariable('T_ROB1', 'user', 'reg1');
70
+ await client.setRapidVariable('T_ROB1', 'user', 'reg1', '42');
61
71
 
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();
72
+ // I/O signals
73
+ const signals = await client.listAllSignals();
74
+ await client.writeSignal('Local', 'DRV_1', 'DO_1', '1');
87
75
 
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
- }
76
+ // Real-time subscriptions
77
+ const unsubscribe = await client.subscribe(
78
+ ['execution', 'controllerstate', { type: 'signal', name: 'Local/DRV_1/DI_1' }],
79
+ (event) => console.log(event.resource, '=', event.value),
80
+ );
81
+ await unsubscribe();
82
+
83
+ await client.disconnect();
95
84
  ```
96
85
 
97
86
  ---
98
87
 
99
88
  ## API Reference
100
89
 
101
- ### `new RwsClient(options)`
90
+ ### Constructor
91
+
92
+ ```ts
93
+ new RwsClient(options: RwsClientOptions)
94
+ ```
102
95
 
103
96
  | Option | Type | Default | Description |
104
- |---|---|---|---|
97
+ |--------|------|---------|-------------|
105
98
  | `host` | `string` | — | Controller IP or hostname |
106
99
  | `port` | `number` | `80` | HTTP port |
107
100
  | `username` | `string` | `'Default User'` | RWS username |
108
101
  | `password` | `string` | `'robotics'` | RWS password |
109
- | `requestIntervalMs` | `number` | `55` | Minimum ms between requests (enforces < 20 req/sec) |
102
+ | `requestIntervalMs` | `number` | `55` | Min ms between requests (enforces < 20 req/sec) |
110
103
  | `timeout` | `number` | `5000` | Request timeout in ms |
104
+ | `sessionCookie` | `string` | — | Saved cookie to reuse an existing session slot |
111
105
 
112
106
  ---
113
107
 
114
108
  ### Connection
115
109
 
116
110
  | Method | Returns | Description |
117
- |---|---|---|
118
- | `connect()` | `Promise<void>` | Establish session and authenticate |
119
- | `disconnect()` | `Promise<void>` | Close subscriptions and clear session |
111
+ |--------|---------|-------------|
112
+ | `connect()` | `void` | Establish session and authenticate |
113
+ | `disconnect()` | `void` | Close WebSocket subscriptions and clear session |
114
+ | `getSessionCookie()` | `string \| null` | Current session cookie — persist to avoid 70-session limit |
120
115
 
121
116
  ---
122
117
 
123
- ### Controller State
118
+ ### Controller State & Panel
124
119
 
125
120
  | Method | Returns | Description |
126
- |---|---|---|
127
- | `getControllerState()` | `Promise<ControllerState>` | `'init'` \| `'motoroff'` \| `'motoron'` \| `'guardstop'` \| `'emergencystop'` \| `'emergencystopreset'` \| `'sysfail'` |
128
- | `getOperationMode()` | `Promise<OperationMode>` | `'AUTO'` \| `'MANR'` \| `'MANF'` |
121
+ |--------|---------|-------------|
122
+ | `getControllerState()` | `ControllerState` | Motor state: motoron / motoroff / guardstop / emergencystop / |
123
+ | `setControllerState(state)` | `void` | Set motor state requires mastership |
124
+ | `getOperationMode()` | `OperationMode` | AUTO / MANR / MANF |
125
+ | `lockOperationMode(pin, permanent?)` | `void` | Lock FlexPendant key switch with PIN |
126
+ | `unlockOperationMode()` | `void` | Unlock FlexPendant key switch |
127
+ | `getSpeedRatio()` | `number` | Speed override 0–100 |
128
+ | `setSpeedRatio(ratio)` | `void` | Set speed override — AUTO mode only |
129
+ | `getCollisionDetectionState()` | `CollisionDetectionState` | INIT / TRIGGERED / CONFIRMED / TRIGGERED_ACK |
130
+ | `restartController(mode?)` | `void` | restart / istart / pstart / bstart |
131
+ | `getControllerIdentity()` | `ControllerIdentity` | Name, ID, type, MAC address |
132
+ | `getSystemInfo()` | `SystemInfo` | RobotWare version, options, system ID |
133
+ | `getControllerClock()` | `ControllerClock` | Current date/time (UTC) |
134
+ | `setControllerClock(y,mo,d,h,mi,s)` | `void` | Set controller date/time (UTC) |
129
135
 
130
136
  ---
131
137
 
132
138
  ### RAPID Execution
133
139
 
134
140
  | 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
+ | `getRapidExecutionState()` | `ExecutionState` | `'running'` \| `'stopped'` |
143
+ | `getRapidExecutionInfo()` | `ExecutionInfo` | State + current cycle mode |
144
+ | `getRapidTasks()` | `RapidTask[]` | All tasks with name, type, state, active flag |
145
+ | `startRapid()` | `void` | Start execution (AUTO + motors on required) |
146
+ | `stopRapid()` | `void` | Stop execution |
147
+ | `resetRapid()` | `void` | Reset program pointer to Main |
148
+ | `setExecutionCycle(cycle)` | `void` | `'once'` \| `'forever'` \| `'asis'` |
149
+ | `activateRapidTask(task)` | `void` | Activate a task (multitasking) |
150
+ | `deactivateRapidTask(task)` | `void` | Deactivate a task |
151
+ | `activateAllRapidTasks()` | `void` | Activate all tasks |
152
+ | `deactivateAllRapidTasks()` | `void` | Deactivate all tasks |
153
+
154
+ ---
155
+
156
+ ### RAPID Variables & Symbols
157
+
158
+ | Method | Returns | Description |
159
+ |--------|---------|-------------|
160
+ | `getRapidVariable(task, module, symbol)` | `string` | Read variable as RAPID-syntax string |
161
+ | `setRapidVariable(task, module, symbol, value)` | `void` | Write variable (RAPID syntax: `'42'`, `'"hello"'`, `'[1,0,0,0]'`) |
162
+ | `validateRapidValue(task, value, datatype)` | `boolean` | Validate value against datatype before writing |
163
+ | `getRapidSymbolProperties(task, module, symbol)` | `RapidSymbolProperties` | Type, dims, storage class, flags |
164
+ | `searchRapidSymbols(params)` | `RapidSymbolInfo[]` | Search by task, type, datatype, or regex |
165
+ | `getActiveUiInstruction()` | `UiInstruction \| null` | Detect if RAPID is waiting for operator input |
166
+ | `setUiInstructionParam(stackurl, param, value)` | `void` | Respond to a UI instruction (TPReadNum, TPReadFK…) |
167
+
168
+ ---
169
+
170
+ ### RAPID Modules
171
+
172
+ | Method | Returns | Description |
173
+ |--------|---------|-------------|
174
+ | `listModules(taskName)` | `string[]` | Names of all loaded modules in a task |
175
+ | `loadModule(task, modulePath, replace?)` | `void` | Load module from controller filesystem into task |
176
+ | `unloadModule(task, moduleName)` | `void` | Unload module from task (RAPID must be stopped) |
141
177
 
142
178
  ---
143
179
 
144
180
  ### Motion
145
181
 
146
182
  | 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) |
183
+ |--------|---------|-------------|
184
+ | `getJointPositions(mechunit?)` | `JointTarget` | rax_1–rax_6 in degrees (default mechunit: ROB_1) |
185
+ | `getCartesianPosition(mechunit?, tool?, wobj?)` | `RobTarget` | TCP x/y/z (mm) + q1–q4 quaternion |
186
+ | `getCartesianFull(mechunit?)` | `CartesianFull` | TCP pose + j1/j4/j6/jx configuration flags |
150
187
 
151
188
  ---
152
189
 
153
- ### Modules
190
+ ### File System
154
191
 
155
192
  | 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 |
193
+ |--------|---------|-------------|
194
+ | `listDirectory(remotePath)` | `FileEntry[]` | Browse a directory (`$HOME`, `$TEMP`, …) |
195
+ | `readFile(remotePath)` | `string` | Download file as UTF-8 string |
196
+ | `uploadModule(remotePath, content)` | `void` | Upload file content (max 800 MB) |
197
+ | `deleteFile(remotePath)` | `void` | Delete file |
198
+ | `createDirectory(parentPath, dirName)` | `void` | Create directory |
199
+ | `copyFile(sourcePath, destPath)` | `void` | Copy file on controller |
160
200
 
161
201
  ---
162
202
 
163
203
  ### I/O Signals
164
204
 
165
205
  | 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 |
206
+ |--------|---------|-------------|
207
+ | `listAllSignals(start?, limit?)` | `Signal[]` | Paginated flat list of all signals |
208
+ | `readSignal(network, device, name)` | `Signal` | Read a specific signal by address |
209
+ | `writeSignal(network, device, name, value)` | `void` | Write DO/AO/GO — value as string: `'1'`, `'0'`, `'3.14'` |
210
+ | `listNetworks()` | `IoNetwork[]` | All I/O networks |
211
+ | `listDevices(network)` | `IoDevice[]` | Devices on a network |
212
+
213
+ > Pass `''` for `network` and `device` to use the flat signal path (works by signal name alone).
214
+
215
+ ---
216
+
217
+ ### Event Log
218
+
219
+ | Method | Returns | Description |
220
+ |--------|---------|-------------|
221
+ | `getEventLog(domain?, lang?)` | `ElogMessage[]` | Read log messages (domain 0 = main, newest first) |
222
+ | `clearEventLog(domain?)` | `void` | Clear messages in one domain |
223
+ | `clearAllEventLogs()` | `void` | Clear all domains |
169
224
 
170
225
  ---
171
226
 
172
- ### Subscriptions
227
+ ### Mastership
228
+
229
+ Required before modifying motor state, speed ratio, or certain RAPID operations.
230
+
231
+ | Method | Returns | Description |
232
+ |--------|---------|-------------|
233
+ | `requestMastership(domain)` | `void` | Take control — `'cfg'` \| `'motion'` \| `'rapid'` |
234
+ | `releaseMastership(domain)` | `void` | Release control — always call in `finally` |
173
235
 
174
236
  ```ts
175
- subscribe(
176
- resources: SubscriptionResource[],
177
- handler: (event: SubscriptionEvent) => void
178
- ): Promise<() => Promise<void>>
237
+ await client.requestMastership('rapid');
238
+ try {
239
+ await client.setControllerState('motoron');
240
+ } finally {
241
+ await client.releaseMastership('rapid');
242
+ }
179
243
  ```
180
244
 
181
- Subscribe to real-time RWS events. Returns an async unsubscribe function.
245
+ ---
182
246
 
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
247
+ ### WebSocket Subscriptions
189
248
 
190
- **`SubscriptionEvent`**:
191
249
  ```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
- }
250
+ const unsubscribe = await client.subscribe(resources, handler);
251
+ // ...
252
+ await unsubscribe();
197
253
  ```
198
254
 
255
+ | Resource | Type | Description |
256
+ |----------|------|-------------|
257
+ | `'execution'` | string | RAPID execution state changes |
258
+ | `'controllerstate'` | string | Motor state changes |
259
+ | `'operationmode'` | string | Operation mode changes |
260
+ | `'speedratio'` | string | Speed ratio changes |
261
+ | `'coldetstate'` | string | Collision detection state |
262
+ | `'uiinstr'` | string | Active UI instruction changes |
263
+ | `{ type: 'execycle' }` | object | Execution cycle mode changes |
264
+ | `{ type: 'taskchange', task }` | object | Task state changes |
265
+ | `{ type: 'signal', name }` | object | I/O signal value changes |
266
+ | `{ type: 'persvar', name }` | object | RAPID persistent variable changes |
267
+ | `{ type: 'elog', domain }` | object | New event log entries |
268
+
269
+ `SubscriptionEvent`: `{ resource: string, value: string, timestamp: Date }`
270
+
199
271
  ---
200
272
 
201
273
  ### Error Handling
202
274
 
203
- All public methods throw `RwsError` (never plain `Error`) with a typed `code`:
275
+ All public methods throw `RwsError` never a plain `Error`.
204
276
 
205
277
  | Code | Meaning |
206
- |---|---|
278
+ |------|---------|
207
279
  | `'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 |
280
+ | `'SESSION_EXPIRED'` | Session timed out |
281
+ | `'MOTORS_OFF'` | Action requires motors on |
282
+ | `'MODULE_NOT_FOUND'` | Module file not found on controller |
283
+ | `'CONTROLLER_BUSY'` | Controller returned 503 retry later |
212
284
  | `'RATE_LIMITED'` | Too many requests (429) |
213
- | `'NETWORK_ERROR'` | TCP/timeout/WebSocket error |
285
+ | `'NETWORK_ERROR'` | TCP / timeout / WebSocket error |
214
286
  | `'PARSE_ERROR'` | Unexpected XML response format |
215
- | `'UNKNOWN'` | Unmapped error; check `httpStatus` and `rwsDetail` |
287
+ | `'UNKNOWN'` | Unmapped error check `httpStatus` and `rwsDetail` |
216
288
 
217
289
  ```ts
218
290
  try {
219
291
  await client.startRapid();
220
292
  } catch (e) {
221
293
  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
- }
294
+ if (e.code === 'MOTORS_OFF') console.error('Enable motors first');
295
+ else throw e;
232
296
  }
233
297
  }
234
298
  ```
235
299
 
236
300
  ---
237
301
 
238
- ## Compatibility
302
+ ## Session Persistence
303
+
304
+ The IRC5 controller allows a maximum of **70 concurrent RWS sessions**. Persist the session cookie across restarts to always reuse the same slot:
239
305
 
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) | ✗ |
306
+ ```ts
307
+ import fs from 'fs';
308
+
309
+ // Save after connecting
310
+ const cookie = client.getSessionCookie();
311
+ if (cookie) fs.writeFileSync('.rws-session', cookie, 'utf8');
312
+
313
+ // Restore on next start
314
+ let sessionCookie: string | undefined;
315
+ try { sessionCookie = fs.readFileSync('.rws-session', 'utf8'); } catch {}
316
+
317
+ const client = new RwsClient({ host, username, password, sessionCookie });
318
+ await client.connect(); // reuses the existing session slot
319
+ ```
320
+
321
+ ---
322
+
323
+ ## Rate Limits
324
+
325
+ | Constraint | Value |
326
+ |------------|-------|
327
+ | Max request rate | 20 req/sec |
328
+ | Client enforced interval | 55 ms between requests |
329
+ | Max concurrent sessions | 70 |
330
+ | Session inactivity timeout | 5 minutes |
331
+ | Max session lifetime | 25 minutes |
332
+ | Max file upload | 800 MB |
333
+
334
+ ---
335
+
336
+ ## Compatibility
246
337
 
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.
338
+ | | RobotWare 6.x (RWS 1.0) | RobotWare 7.x (RWS 2.0) |
339
+ |--|--|--|
340
+ | This package | ✅ Supported | ❌ Not compatible |
341
+ | IRC5 controller | ✅ | n/a |
342
+ | OmniCore controller | n/a | ❌ |
343
+ | RobotStudio virtual | ✅ (127.0.0.1) | ❌ |
248
344
 
249
345
  ---
250
346
 
251
347
  ## Resources
252
348
 
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
349
+ - [ABB Developer Center — RWS API](https://developercenter.robotstudio.com/api/rwsApi/)
350
+ - [Companion VS Code Extension](https://marketplace.visualstudio.com/items?itemName=merajsafari.abb-rws)
256
351
 
257
352
  ---
258
353