abb-rws-client 0.2.0 → 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,28 +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
7
 
8
8
  ---
9
9
 
10
10
  ## VS Code Extension
11
11
 
12
- Prefer a GUI? The companion VS Code extension gives you live status, joint positions, RAPID control, and module management directly from the sidebar — no code required.
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
13
 
14
- **[ABB Robot (RWS) — VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=merajsafari.abb-rws-vscode)**
14
+ **[ABB Robot (RWS) — VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=merajsafari.abb-rws)**
15
15
 
16
16
  ---
17
17
 
18
18
  ## Features
19
19
 
20
20
  - HTTP Digest Authentication (RFC 2617) — no external auth libraries
21
- - Session cookie management (`ABBCX`, `-http-session-`)
21
+ - Session cookie management (`ABBCX`, `-http-session-`) with automatic reuse
22
22
  - Request rate limiting (< 20 req/sec, configurable)
23
- - Automatic session re-authentication after 5-minute inactivity
24
- - 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
25
25
  - Auto-reconnect on WebSocket disconnect (3 retries, exponential backoff)
26
26
  - Fully typed public API — every method throws `RwsError` with a typed `code`
27
- - Zero runtime dependencies — Node 18+ built-ins only
27
+ - Zero runtime dependencies — Node.js built-ins only
28
28
 
29
29
  ---
30
30
 
@@ -34,213 +34,265 @@ Prefer a GUI? The companion VS Code extension gives you live status, joint posit
34
34
  npm install abb-rws-client
35
35
  ```
36
36
 
37
- **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`).
38
38
 
39
39
  ---
40
40
 
41
- ## Quick Start — RobotStudio virtual controller
41
+ ## Quick Start
42
42
 
43
43
  ```ts
44
44
  import { RwsClient, RwsError } from 'abb-rws-client';
45
45
 
46
46
  const client = new RwsClient({
47
- host: '127.0.0.1', // RobotStudio default
48
- port: 80,
47
+ host: '192.168.125.1',
49
48
  username: 'Default User',
50
49
  password: 'robotics',
51
50
  });
52
51
 
53
- try {
54
- await client.connect();
52
+ await client.connect();
55
53
 
56
- // Read state
57
- const state = await client.getControllerState();
58
- 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'
59
57
 
60
- const mode = await client.getOperationMode();
61
- 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
62
62
 
63
- // Read positions
64
- const joints = await client.getJointPositions();
65
- console.log('J1:', joints.rax_1, 'degrees');
63
+ // RAPID
64
+ await client.startRapid();
65
+ await client.stopRapid();
66
+ await client.resetRapid(); // PP to Main
66
67
 
67
- const tcp = await client.getCartesianPosition();
68
- 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');
69
71
 
70
- // RAPID execution
71
- await client.startRapid();
72
- await client.stopRapid();
73
-
74
- // I/O signals
75
- const di = await client.readSignal('Local', 'DRV_1', 'DI_1');
76
- console.log('DI_1 =', di.value);
77
- await client.writeSignal('Local', 'DRV_1', 'DO_1', '1');
78
-
79
- // Upload and load a RAPID module
80
- const modSource = `MODULE MyMod\n PROC main()\n TPWrite "Hello";\n ENDPROC\nENDMODULE`;
81
- await client.uploadModule('$HOME/MyMod.mod', modSource);
82
- await client.loadModule('T_ROB1', '$HOME/MyMod.mod');
83
-
84
- // Subscribe to real-time events
85
- const unsubscribe = await client.subscribe(
86
- ['execution', 'controllerstate'],
87
- (event) => {
88
- console.log(`[${event.timestamp.toISOString()}] ${event.resource} = ${event.value}`);
89
- }
90
- );
91
-
92
- // ... later, unsubscribe and disconnect
93
- await unsubscribe();
94
- await client.disconnect();
72
+ // I/O signals
73
+ const signals = await client.listAllSignals();
74
+ await client.writeSignal('Local', 'DRV_1', 'DO_1', '1');
95
75
 
96
- } catch (e) {
97
- if (e instanceof RwsError) {
98
- console.error(`[${e.code}] ${e.message} (HTTP ${e.httpStatus ?? 'N/A'})`);
99
- } else {
100
- throw e;
101
- }
102
- }
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();
103
84
  ```
104
85
 
105
86
  ---
106
87
 
107
88
  ## API Reference
108
89
 
109
- ### `new RwsClient(options)`
90
+ ### Constructor
91
+
92
+ ```ts
93
+ new RwsClient(options: RwsClientOptions)
94
+ ```
110
95
 
111
96
  | Option | Type | Default | Description |
112
- |---|---|---|---|
97
+ |--------|------|---------|-------------|
113
98
  | `host` | `string` | — | Controller IP or hostname |
114
99
  | `port` | `number` | `80` | HTTP port |
115
100
  | `username` | `string` | `'Default User'` | RWS username |
116
101
  | `password` | `string` | `'robotics'` | RWS password |
117
- | `requestIntervalMs` | `number` | `55` | Minimum ms between requests (enforces < 20 req/sec) |
102
+ | `requestIntervalMs` | `number` | `55` | Min ms between requests (enforces < 20 req/sec) |
118
103
  | `timeout` | `number` | `5000` | Request timeout in ms |
119
- | `sessionCookie` | `string` | — | Saved cookie string to reuse an existing session slot (avoids the 70-session controller limit) |
104
+ | `sessionCookie` | `string` | — | Saved cookie to reuse an existing session slot |
120
105
 
121
106
  ---
122
107
 
123
108
  ### Connection
124
109
 
125
110
  | Method | Returns | Description |
126
- |---|---|---|
127
- | `connect()` | `Promise<void>` | Establish session and authenticate |
128
- | `disconnect()` | `Promise<void>` | Close subscriptions and clear session |
129
- | `getSessionCookie()` | `string \| null` | Current session cookie — save and pass as `sessionCookie` on next start to reuse the slot |
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 |
130
115
 
131
116
  ---
132
117
 
133
- ### Controller State
118
+ ### Controller State & Panel
134
119
 
135
120
  | Method | Returns | Description |
136
- |---|---|---|
137
- | `getControllerState()` | `Promise<ControllerState>` | `'init'` \| `'motoroff'` \| `'motoron'` \| `'guardstop'` \| `'emergencystop'` \| `'emergencystopreset'` \| `'sysfail'` |
138
- | `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) |
139
135
 
140
136
  ---
141
137
 
142
138
  ### RAPID Execution
143
139
 
144
140
  | Method | Returns | Description |
145
- |---|---|---|
146
- | `getRapidExecutionState()` | `Promise<ExecutionState>` | `'running'` \| `'stopped'` |
147
- | `getRapidTasks()` | `Promise<RapidTask[]>` | All RAPID tasks and their states |
148
- | `startRapid()` | `Promise<void>` | Start RAPID execution (requires AUTO + motors on) |
149
- | `stopRapid()` | `Promise<void>` | Stop RAPID execution |
150
- | `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) |
151
177
 
152
178
  ---
153
179
 
154
180
  ### Motion
155
181
 
156
182
  | Method | Returns | Description |
157
- |---|---|---|
158
- | `getJointPositions(mechunit?)` | `Promise<JointTarget>` | Joint angles in degrees for all 6 axes |
159
- | `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 |
160
187
 
161
188
  ---
162
189
 
163
- ### Modules
190
+ ### File System
164
191
 
165
192
  | Method | Returns | Description |
166
- |---|---|---|
167
- | `uploadModule(remotePath, content)` | `Promise<void>` | Upload RAPID `.mod` source to controller filesystem |
168
- | `loadModule(taskName, modulePath)` | `Promise<void>` | Load an uploaded module into a RAPID task |
169
- | `unloadModule(taskName, moduleName)` | `Promise<void>` | Unload a module from a task (RAPID must be stopped) |
170
- | `listModules(taskName)` | `Promise<string[]>` | Names of all loaded modules in a task |
171
- | `readFile(remotePath)` | `Promise<string>` | Download a file from the controller filesystem as a string |
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 |
172
200
 
173
201
  ---
174
202
 
175
203
  ### I/O Signals
176
204
 
177
205
  | Method | Returns | Description |
178
- |---|---|---|
179
- | `readSignal(network, device, name)` | `Promise<Signal>` | Read current signal value |
180
- | `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 |
181
224
 
182
225
  ---
183
226
 
184
- ### 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` |
185
235
 
186
236
  ```ts
187
- subscribe(
188
- resources: SubscriptionResource[],
189
- handler: (event: SubscriptionEvent) => void
190
- ): Promise<() => Promise<void>>
237
+ await client.requestMastership('rapid');
238
+ try {
239
+ await client.setControllerState('motoron');
240
+ } finally {
241
+ await client.releaseMastership('rapid');
242
+ }
191
243
  ```
192
244
 
193
- Subscribe to real-time RWS events. Returns an async unsubscribe function.
245
+ ---
194
246
 
195
- **`SubscriptionResource`** can be:
196
- - `'execution'` — RAPID execution state changes
197
- - `'controllerstate'` — controller state changes
198
- - `'operationmode'` — operation mode changes
199
- - `{ type: 'signal'; name: 'network/device/signalname' }` — I/O signal changes
200
- - `{ type: 'persvar'; name: 'task/varname' }` — RAPID persistent variable changes
247
+ ### WebSocket Subscriptions
201
248
 
202
- **`SubscriptionEvent`**:
203
249
  ```ts
204
- {
205
- resource: string; // RWS path of the changed resource
206
- value: string; // New value as string
207
- timestamp: Date; // When the event was received
208
- }
250
+ const unsubscribe = await client.subscribe(resources, handler);
251
+ // ...
252
+ await unsubscribe();
209
253
  ```
210
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
+
211
271
  ---
212
272
 
213
273
  ### Error Handling
214
274
 
215
- All public methods throw `RwsError` (never plain `Error`) with a typed `code`:
275
+ All public methods throw `RwsError` never a plain `Error`.
216
276
 
217
277
  | Code | Meaning |
218
- |---|---|
278
+ |------|---------|
219
279
  | `'AUTH_FAILED'` | Wrong credentials or session rejected |
220
- | `'SESSION_EXPIRED'` | Session timed out and re-auth was not possible |
221
- | `'MOTORS_OFF'` | Action requires motors to be on |
222
- | `'MODULE_NOT_FOUND'` | Module file not found on controller filesystem |
223
- | `'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 |
224
284
  | `'RATE_LIMITED'` | Too many requests (429) |
225
- | `'NETWORK_ERROR'` | TCP/timeout/WebSocket error |
285
+ | `'NETWORK_ERROR'` | TCP / timeout / WebSocket error |
226
286
  | `'PARSE_ERROR'` | Unexpected XML response format |
227
- | `'UNKNOWN'` | Unmapped error; check `httpStatus` and `rwsDetail` |
287
+ | `'UNKNOWN'` | Unmapped error check `httpStatus` and `rwsDetail` |
228
288
 
229
289
  ```ts
230
290
  try {
231
291
  await client.startRapid();
232
292
  } catch (e) {
233
293
  if (e instanceof RwsError) {
234
- switch (e.code) {
235
- case 'MOTORS_OFF':
236
- console.error('Enable motors on the FlexPendant first');
237
- break;
238
- case 'AUTH_FAILED':
239
- console.error('Check username/password');
240
- break;
241
- default:
242
- console.error(`Unexpected error [${e.code}]: ${e.message}`);
243
- }
294
+ if (e.code === 'MOTORS_OFF') console.error('Enable motors first');
295
+ else throw e;
244
296
  }
245
297
  }
246
298
  ```
@@ -249,43 +301,53 @@ try {
249
301
 
250
302
  ## Session Persistence
251
303
 
252
- The IRC5 controller allows a maximum of **70 concurrent RWS sessions**. To avoid exhausting this limit across process restarts, persist the session cookie:
304
+ The IRC5 controller allows a maximum of **70 concurrent RWS sessions**. Persist the session cookie across restarts to always reuse the same slot:
253
305
 
254
306
  ```ts
255
307
  import fs from 'fs';
256
308
 
257
- // After connecting — save the cookie
309
+ // Save after connecting
258
310
  const cookie = client.getSessionCookie();
259
311
  if (cookie) fs.writeFileSync('.rws-session', cookie, 'utf8');
260
312
 
261
- // On next start — reuse it
313
+ // Restore on next start
262
314
  let sessionCookie: string | undefined;
263
315
  try { sessionCookie = fs.readFileSync('.rws-session', 'utf8'); } catch {}
264
316
 
265
317
  const client = new RwsClient({ host, username, password, sessionCookie });
266
- await client.connect(); // reuses the existing slot, no new session created
318
+ await client.connect(); // reuses the existing session slot
267
319
  ```
268
320
 
269
321
  ---
270
322
 
271
- ## Compatibility
323
+ ## Rate Limits
272
324
 
273
- | Feature | RobotWare 6.x (RWS 1.0) | RobotWare 7.x (RWS 2.0) |
274
- |---|---|---|
275
- | This package | ✓ Supported | Not compatible |
276
- | Controller: IRC5 | | n/a |
277
- | Controller: OmniCore | n/a | |
278
- | RobotStudio (virtual) | (127.0.0.1) | ✗ |
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
279
337
 
280
- 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) | ❌ |
281
344
 
282
345
  ---
283
346
 
284
347
  ## Resources
285
348
 
286
- - [ABB RWS 1.0 API Reference](https://developercenter.robotstudio.com/api/rwsApi/)
287
- - [ABB Developer Center](https://developercenter.robotstudio.com/api/RWS)
288
- - [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)
289
351
 
290
352
  ---
291
353