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.
Files changed (70) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +17 -8
  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
@@ -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 { createClient } from 'abb-rws-client';
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
- const client = await createClient({
15
- host: process.env.RWS_HOST || '127.0.0.1',
16
- port: Number(process.env.RWS_PORT) || undefined,
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
- // Wrap with RobotManager only if you need lifecycle features. For a one-shot
20
- // pull, the raw client is enough.
21
- const isRws2 = client.constructor.name === 'RwsClient2';
22
- let source;
23
- if (isRws2) {
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 client.disconnect();
31
+ await robot.disconnect();
package/package.json CHANGED
@@ -1,60 +1,62 @@
1
- {
2
- "name": "abb-rws-client",
3
- "version": "0.7.2",
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/merajsafari/abb-rws-client",
18
- "repository": {
19
- "type": "git",
20
- "url": "https://github.com/merajsafari/abb-rws-client.git"
21
- },
22
- "type": "module",
23
- "main": "dist/index.js",
24
- "types": "dist/index.d.ts",
25
- "exports": {
26
- ".": {
27
- "import": "./dist/index.js",
28
- "types": "./dist/index.d.ts"
29
- }
30
- },
31
- "files": [
32
- "dist",
33
- "examples",
34
- "README.md",
35
- "LICENSE",
36
- "CHANGELOG.md"
37
- ],
38
- "engines": {
39
- "node": ">=18"
40
- },
41
- "scripts": {
42
- "build": "tsc",
43
- "test": "vitest run",
44
- "test:watch": "vitest",
45
- "lint": "eslint src",
46
- "prepublishOnly": "npm run build && npm test"
47
- },
48
- "devDependencies": {
49
- "@types/node": "^20.0.0",
50
- "@types/ws": "^8.18.1",
51
- "@typescript-eslint/eslint-plugin": "^8.0.0",
52
- "@typescript-eslint/parser": "^8.0.0",
53
- "eslint": "^9.0.0",
54
- "typescript": "^5.4.0",
55
- "vitest": "^1.6.1"
56
- },
57
- "dependencies": {
58
- "ws": "^8.20.0"
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&amp;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
+ }