abb-rws-client 0.7.2 → 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.
- package/CHANGELOG.md +137 -0
- package/README.md +17 -8
- package/dist/HalJsonParser.d.ts +55 -0
- package/dist/HalJsonParser.d.ts.map +1 -0
- package/dist/HalJsonParser.js +155 -0
- package/dist/HalJsonParser.js.map +1 -0
- package/dist/HttpSession.d.ts.map +1 -1
- package/dist/HttpSession.js +13 -2
- package/dist/HttpSession.js.map +1 -1
- package/dist/IRWSAdapter.d.ts +7 -1
- package/dist/IRWSAdapter.d.ts.map +1 -1
- package/dist/MdnsDiscovery.d.ts +57 -0
- package/dist/MdnsDiscovery.d.ts.map +1 -0
- package/dist/MdnsDiscovery.js +313 -0
- package/dist/MdnsDiscovery.js.map +1 -0
- package/dist/MultiRobotManager.d.ts +5 -2
- package/dist/MultiRobotManager.d.ts.map +1 -1
- package/dist/MultiRobotManager.js +8 -3
- package/dist/MultiRobotManager.js.map +1 -1
- package/dist/RWS1Adapter.d.ts +60 -2
- package/dist/RWS1Adapter.d.ts.map +1 -1
- package/dist/RWS1Adapter.js +152 -4
- package/dist/RWS1Adapter.js.map +1 -1
- package/dist/ResourceMapper.d.ts +4 -0
- package/dist/ResourceMapper.d.ts.map +1 -1
- package/dist/ResourceMapper.js +9 -1
- package/dist/ResourceMapper.js.map +1 -1
- package/dist/RobotManager.d.ts +72 -12
- package/dist/RobotManager.d.ts.map +1 -1
- package/dist/RobotManager.js +255 -50
- package/dist/RobotManager.js.map +1 -1
- package/dist/RwsClient2.d.ts +150 -10
- package/dist/RwsClient2.d.ts.map +1 -1
- package/dist/RwsClient2.js +599 -236
- package/dist/RwsClient2.js.map +1 -1
- package/dist/WsSubscriber.d.ts +31 -5
- package/dist/WsSubscriber.d.ts.map +1 -1
- package/dist/WsSubscriber.js +104 -50
- package/dist/WsSubscriber.js.map +1 -1
- package/dist/detect.d.ts +12 -3
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js +69 -25
- package/dist/detect.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/types.js +3 -3
- package/dist/types.js.map +1 -1
- package/examples/05-remote-control-rmmp.mjs +10 -12
- package/examples/06-pull-module-source.mjs +13 -20
- package/package.json +62 -60
- package/src/HalJsonParser.ts +137 -0
- package/src/HttpSession.ts +460 -0
- package/src/IRWSAdapter.ts +422 -0
- package/src/Logger.ts +54 -0
- package/src/MdnsDiscovery.ts +336 -0
- package/src/MultiRobotManager.ts +159 -0
- package/src/RWS1Adapter.ts +1018 -0
- package/src/RWS2Adapter.ts +19 -0
- package/src/ResourceMapper.ts +517 -0
- package/src/ResponseParser.ts +710 -0
- package/src/RobotManager.ts +1705 -0
- package/src/RwsClient.ts +1150 -0
- package/src/RwsClient2.ts +2214 -0
- package/src/WsSubscriber.ts +350 -0
- package/src/XhtmlParser.ts +53 -0
- package/src/detect.ts +261 -0
- package/src/index.ts +83 -0
- package/src/types.ts +336 -0
|
@@ -5,34 +5,27 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Run: RWS_HOST=192.168.125.1 node examples/06-pull-module-source.mjs MotionTest
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import { RobotManager, RWS2Adapter } from 'abb-rws-client';
|
|
8
|
+
import { RobotManager } from 'abb-rws-client';
|
|
10
9
|
import * as fs from 'node:fs/promises';
|
|
11
10
|
|
|
12
11
|
const moduleName = process.argv[2] ?? 'user';
|
|
12
|
+
const host = process.env.RWS_HOST || '127.0.0.1';
|
|
13
|
+
const port = process.env.RWS_PORT ? Number(process.env.RWS_PORT) : undefined;
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
// RobotManager auto-detects RWS 1.0 vs 2.0 and exposes getModuleSource()
|
|
16
|
+
// uniformly over both protocols (it reads via the controller's fileservice).
|
|
17
|
+
const robot = new RobotManager();
|
|
18
|
+
await robot.connect(host, process.env.RWS_USER || 'Admin', process.env.RWS_PASS || 'robotics', port);
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// RwsClient2 doesn't expose getModuleSource directly — go through the adapter.
|
|
25
|
-
const robot = new RobotManager({ adapter: new RWS2Adapter(client) });
|
|
26
|
-
await robot.start();
|
|
27
|
-
source = await robot.getModuleSource('T_ROB1', moduleName);
|
|
28
|
-
await robot.stop();
|
|
29
|
-
} else {
|
|
30
|
-
// RWS 1.0 has it on the client.
|
|
31
|
-
source = await client.getModuleSource?.('T_ROB1', moduleName) ?? '';
|
|
20
|
+
const source = await robot.getModuleSource('T_ROB1', moduleName);
|
|
21
|
+
if (!source) {
|
|
22
|
+
console.error(`✗ Module '${moduleName}' not found (or empty) in task T_ROB1.`);
|
|
23
|
+
await robot.disconnect();
|
|
24
|
+
process.exit(1);
|
|
32
25
|
}
|
|
33
26
|
|
|
34
27
|
await fs.writeFile(`${moduleName}.mod`, source, 'utf8');
|
|
35
28
|
console.log(`✓ Wrote ${moduleName}.mod (${source.length} bytes)`);
|
|
36
29
|
console.log('Now: git diff, edit, git commit, push back via loadModule.');
|
|
37
30
|
|
|
38
|
-
await
|
|
31
|
+
await robot.disconnect();
|
package/package.json
CHANGED
|
@@ -1,60 +1,62 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "abb-rws-client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Typed TypeScript/Node.js client for ABB Robot Web Services — supports both RWS 1.0 (IRC5/RobotWare 6) and RWS 2.0 (OmniCore/RobotWare 7).",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"abb",
|
|
7
|
-
"robot",
|
|
8
|
-
"rws",
|
|
9
|
-
"robot-web-services",
|
|
10
|
-
"irc5",
|
|
11
|
-
"omnicore",
|
|
12
|
-
"robotstudio",
|
|
13
|
-
"robotware",
|
|
14
|
-
"industrial-robot"
|
|
15
|
-
],
|
|
16
|
-
"license": "MIT",
|
|
17
|
-
"homepage": "https://github.com/
|
|
18
|
-
"repository": {
|
|
19
|
-
"type": "git",
|
|
20
|
-
"url": "https://github.com/
|
|
21
|
-
},
|
|
22
|
-
"type": "module",
|
|
23
|
-
"main": "dist/index.js",
|
|
24
|
-
"types": "dist/index.d.ts",
|
|
25
|
-
"exports": {
|
|
26
|
-
".": {
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
"files": [
|
|
32
|
-
"dist",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
"test
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"@
|
|
52
|
-
"@
|
|
53
|
-
"eslint": "^
|
|
54
|
-
"typescript": "^
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "abb-rws-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Typed TypeScript/Node.js client for ABB Robot Web Services — supports both RWS 1.0 (IRC5/RobotWare 6) and RWS 2.0 (OmniCore/RobotWare 7).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"abb",
|
|
7
|
+
"robot",
|
|
8
|
+
"rws",
|
|
9
|
+
"robot-web-services",
|
|
10
|
+
"irc5",
|
|
11
|
+
"omnicore",
|
|
12
|
+
"robotstudio",
|
|
13
|
+
"robotware",
|
|
14
|
+
"industrial-robot"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"homepage": "https://github.com/ichbinmeraj/abb-rws-client",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/ichbinmeraj/abb-rws-client.git"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "dist/index.js",
|
|
24
|
+
"types": "dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"src",
|
|
34
|
+
"examples",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"CHANGELOG.md"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:watch": "vitest",
|
|
46
|
+
"lint": "eslint src",
|
|
47
|
+
"prepack": "npm run build && npm test",
|
|
48
|
+
"prepublishOnly": "npm run build && npm test"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^20.0.0",
|
|
52
|
+
"@types/ws": "^8.18.1",
|
|
53
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
54
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
55
|
+
"eslint": "^9.0.0",
|
|
56
|
+
"typescript": "^5.4.0",
|
|
57
|
+
"vitest": "^1.6.1"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"ws": "^8.20.0"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
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
|
+
|
|
24
|
+
type JsonNode = string | number | boolean | null | JsonNode[] | { [key: string]: JsonNode };
|
|
25
|
+
type JsonObject = { [key: string]: JsonNode };
|
|
26
|
+
|
|
27
|
+
export class HalJsonParser {
|
|
28
|
+
private readonly root: JsonNode | null;
|
|
29
|
+
|
|
30
|
+
constructor(body: string) {
|
|
31
|
+
let parsed: JsonNode | null = null;
|
|
32
|
+
try { parsed = JSON.parse(body) as JsonNode; } catch { parsed = null; }
|
|
33
|
+
this.root = parsed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Cheap sniff used to pick a parser: HAL bodies are single JSON objects. */
|
|
37
|
+
static looksLikeJson(body: string): boolean {
|
|
38
|
+
return body.trimStart().startsWith('{');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Returns the field map of the first resource whose `_type` matches. */
|
|
42
|
+
getState(type: string): Record<string, string> {
|
|
43
|
+
return this.getAllStates(type)[0] ?? {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Returns field maps for every resource with a matching `_type`, in document order. */
|
|
47
|
+
getAllStates(type: string): Array<Record<string, string>> {
|
|
48
|
+
const results: Array<Record<string, string>> = [];
|
|
49
|
+
this.walk(this.root, obj => {
|
|
50
|
+
if (obj['_type'] === type) { results.push(HalJsonParser.flatten(obj)); }
|
|
51
|
+
});
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** First scalar value stored under `key` in any resource (XhtmlParser.get analogue). */
|
|
56
|
+
get(key: string): string | undefined {
|
|
57
|
+
let found: string | undefined;
|
|
58
|
+
this.walk(this.root, obj => {
|
|
59
|
+
if (found !== undefined) { return; }
|
|
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
|
+
/**
|
|
69
|
+
* Error details from the top-level status block. Success responses carry a
|
|
70
|
+
* positive code (294912 observed live); errors a negative code plus `msg`.
|
|
71
|
+
*/
|
|
72
|
+
getError(): { code: string; msg: string } | null {
|
|
73
|
+
const status = this.rootObject()?.['status'];
|
|
74
|
+
if (!status || typeof status !== 'object' || Array.isArray(status)) { return null; }
|
|
75
|
+
const code = (status as JsonObject)['code'];
|
|
76
|
+
if (typeof code !== 'number' || code >= 0) { return null; }
|
|
77
|
+
const msg = (status as JsonObject)['msg'];
|
|
78
|
+
return { code: String(code), msg: typeof msg === 'string' ? msg : '' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Pagination link (`_links.next.href`), raw as sent. Live-verified quirk:
|
|
83
|
+
* the controller XML-escapes ampersands even inside JSON strings
|
|
84
|
+
* (`"signals?start=3&limit=3"`) — callers must unescape, exactly as on
|
|
85
|
+
* the XHTML `rel="next"` path.
|
|
86
|
+
*/
|
|
87
|
+
nextHref(): string | undefined {
|
|
88
|
+
const links = this.rootObject()?.['_links'];
|
|
89
|
+
if (!links || typeof links !== 'object' || Array.isArray(links)) { return undefined; }
|
|
90
|
+
const next = (links as JsonObject)['next'];
|
|
91
|
+
if (!next || typeof next !== 'object' || Array.isArray(next)) { return undefined; }
|
|
92
|
+
const href = (next as JsonObject)['href'];
|
|
93
|
+
return typeof href === 'string' && href ? href : undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private rootObject(): JsonObject | null {
|
|
97
|
+
return this.root && typeof this.root === 'object' && !Array.isArray(this.root)
|
|
98
|
+
? this.root as JsonObject
|
|
99
|
+
: null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Depth-first visit of every object in the tree, skipping `_links` blocks. */
|
|
103
|
+
private walk(node: JsonNode | null, visit: (obj: JsonObject) => void): void {
|
|
104
|
+
if (Array.isArray(node)) {
|
|
105
|
+
for (const item of node) { this.walk(item, visit); }
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (node && typeof node === 'object') {
|
|
109
|
+
visit(node as JsonObject);
|
|
110
|
+
for (const [key, value] of Object.entries(node)) {
|
|
111
|
+
if (key === '_links') { continue; }
|
|
112
|
+
if (value && typeof value === 'object') { this.walk(value, visit); }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Scalar fields → strings; `_title` kept; self link exposed as `_href` (XHTML parity). */
|
|
118
|
+
private static flatten(obj: JsonObject): Record<string, string> {
|
|
119
|
+
const fields: Record<string, string> = {};
|
|
120
|
+
const title = obj['_title'];
|
|
121
|
+
if (typeof title === 'string') { fields['_title'] = title; }
|
|
122
|
+
const links = obj['_links'];
|
|
123
|
+
if (links && typeof links === 'object' && !Array.isArray(links)) {
|
|
124
|
+
const self = (links as JsonObject)['self'];
|
|
125
|
+
if (self && typeof self === 'object' && !Array.isArray(self)) {
|
|
126
|
+
const href = (self as JsonObject)['href'];
|
|
127
|
+
if (typeof href === 'string') { fields['_href'] = href; }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
131
|
+
if (key.startsWith('_')) { continue; }
|
|
132
|
+
if (value === null || typeof value === 'object') { continue; } // nested resources have their own _type
|
|
133
|
+
fields[key] = String(value);
|
|
134
|
+
}
|
|
135
|
+
return fields;
|
|
136
|
+
}
|
|
137
|
+
}
|