@tteja/node-red-dashboard-2-robot-arm 0.3.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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/docs/ARCHITECTURE.md +51 -0
  4. package/examples/01-inject-demo.json +133 -0
  5. package/examples/02-controller-to-robot.json +72 -0
  6. package/examples/03-rosbridge-live.json +54 -0
  7. package/examples/04-controller-and-ros2.json +72 -0
  8. package/examples/05-dashboard2.json +143 -0
  9. package/lib/normalize.js +146 -0
  10. package/lib/robot-runtime.js +144 -0
  11. package/lib/rosbridge.js +210 -0
  12. package/models/ur5e/LICENSE +33 -0
  13. package/models/ur5e/README.md +15 -0
  14. package/models/ur5e/joints.json +12 -0
  15. package/models/ur5e/meshes/base.stl +0 -0
  16. package/models/ur5e/meshes/forearm.stl +0 -0
  17. package/models/ur5e/meshes/shoulder.stl +0 -0
  18. package/models/ur5e/meshes/upperarm.stl +0 -0
  19. package/models/ur5e/meshes/wrist1.stl +0 -0
  20. package/models/ur5e/meshes/wrist2.stl +0 -0
  21. package/models/ur5e/meshes/wrist3.stl +0 -0
  22. package/models/ur5e/ur5e.urdf +138 -0
  23. package/models/ur5e-gripper/LICENSE +33 -0
  24. package/models/ur5e-gripper/README-ur5e.md +15 -0
  25. package/models/ur5e-gripper/README.md +6 -0
  26. package/models/ur5e-gripper/joints.json +13 -0
  27. package/models/ur5e-gripper/meshes/base.stl +0 -0
  28. package/models/ur5e-gripper/meshes/forearm.stl +0 -0
  29. package/models/ur5e-gripper/meshes/shoulder.stl +0 -0
  30. package/models/ur5e-gripper/meshes/upperarm.stl +0 -0
  31. package/models/ur5e-gripper/meshes/wrist1.stl +0 -0
  32. package/models/ur5e-gripper/meshes/wrist2.stl +0 -0
  33. package/models/ur5e-gripper/meshes/wrist3.stl +0 -0
  34. package/models/ur5e-gripper/ur5e_gripper.urdf +182 -0
  35. package/nodes/controller.html +76 -0
  36. package/nodes/controller.js +237 -0
  37. package/nodes/robot.html +109 -0
  38. package/nodes/robot.js +347 -0
  39. package/nodes/ui-robot-arm.html +82 -0
  40. package/nodes/ui-robot-arm.js +25 -0
  41. package/nodes/ui-robot-controller.html +81 -0
  42. package/nodes/ui-robot-controller.js +25 -0
  43. package/package.json +97 -0
  44. package/resources/controller.html +64 -0
  45. package/resources/controller.js +114 -0
  46. package/resources/pendant.html +66 -0
  47. package/resources/pendant.js +129 -0
  48. package/resources/robot-arm-widgets.umd.js +2 -0
  49. package/resources/viewer.html +54 -0
  50. package/resources/viewer.js +195 -0
  51. package/ui/components/UIRobotArm.vue +63 -0
  52. package/ui/components/UIRobotController.vue +62 -0
  53. package/ui/index.js +2 -0
@@ -0,0 +1,2 @@
1
+ (function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".ui-robot-arm-wrapper[data-v-78e5dc1c]{width:100%;height:100%;min-height:280px;display:flex;flex-direction:column;overflow:hidden;border-radius:6px;background:#0f1419}.ui-robot-arm-frame[data-v-78e5dc1c]{flex:1;width:100%;height:100%;min-height:280px;border:0;background:#0f1419}.ui-robot-arm-empty[data-v-78e5dc1c]{padding:16px;color:#8b9bb4;font-size:13px}.ui-robot-controller-wrapper[data-v-49006e59]{width:100%;height:100%;min-height:280px;display:flex;flex-direction:column;overflow:hidden;border-radius:6px;background:#0f1419}.ui-robot-controller-frame[data-v-49006e59]{flex:1;width:100%;height:100%;min-height:280px;border:0;background:#0f1419}.ui-robot-controller-empty[data-v-49006e59]{padding:16px;color:#8b9bb4;font-size:13px}")),document.head.appendChild(e)}}catch(o){console.error("vite-plugin-css-injected-by-js",o)}})();
2
+ (function(t,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(t=typeof globalThis<"u"?globalThis:t||self,e(t["robot-arm-widgets"]={},t.Vue))})(this,(function(t,e){"use strict";const r=(c,o)=>{const i=c.__vccOpts||c;for(const[a,l]of o)i[a]=l;return i},n={name:"UIRobotArm",inject:["$dataTracker"],props:{id:{type:String,required:!0},props:{type:Object,default:()=>({})},state:{type:Object,default:()=>({enabled:!1,visible:!1})}},computed:{robotId(){return this.props&&this.props.robot||""},viewUrl(){return this.robotId?`/robot-arm/view/${encodeURIComponent(this.robotId)}`:""}},created(){this.$dataTracker(this.id)}},d={class:"ui-robot-arm-wrapper"},p=["src"],b={key:1,class:"ui-robot-arm-empty"};function f(c,o,i,a,l,s){return e.openBlock(),e.createElementBlock("div",d,[s.robotId?(e.openBlock(),e.createElementBlock("iframe",{key:0,class:"ui-robot-arm-frame",src:s.viewUrl,title:"Robot arm 3D view",allow:"fullscreen"},null,8,p)):(e.openBlock(),e.createElementBlock("div",b,[...o[0]||(o[0]=[e.createTextVNode(" Select a ",-1),e.createElementVNode("strong",null,"robot",-1),e.createTextVNode(" node in the widget properties. ",-1)])]))])}const m=r(n,[["render",f],["__scopeId","data-v-78e5dc1c"]]),u={name:"UIRobotController",inject:["$dataTracker"],props:{id:{type:String,required:!0},props:{type:Object,default:()=>({})},state:{type:Object,default:()=>({enabled:!1,visible:!1})}},computed:{robotId(){return this.props&&this.props.robot||""},pendantUrl(){return this.robotId?`/robot-arm/pendant/${encodeURIComponent(this.robotId)}`:""}},created(){this.$dataTracker(this.id)}},_={class:"ui-robot-controller-wrapper"},h=["src"],k={key:1,class:"ui-robot-controller-empty"};function I(c,o,i,a,l,s){return e.openBlock(),e.createElementBlock("div",_,[s.robotId?(e.openBlock(),e.createElementBlock("iframe",{key:0,class:"ui-robot-controller-frame",src:s.pendantUrl,title:"Robot teach pendant"},null,8,h)):(e.openBlock(),e.createElementBlock("div",k,[...o[0]||(o[0]=[e.createTextVNode(" Select a ",-1),e.createElementVNode("strong",null,"robot",-1),e.createTextVNode(" node in the widget properties. ",-1)])]))])}const w=r(u,[["render",I],["__scopeId","data-v-49006e59"]]);t.UIRobotArm=m,t.UIRobotController=w,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})}));(function(){var t=window["robot-arm-widgets"]||{};function e(r,n){var d=window[r]||(window[r]={});t[n]&&(d[n]=t[n])}e("ui-ur5e-arm","UIRobotArm"),e("ui-ur5e-controller","UIRobotController")})();
@@ -0,0 +1,54 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Robot Arm Viewer</title>
7
+ <style>
8
+ * { box-sizing: border-box; }
9
+ html, body { margin: 0; height: 100%; overflow: hidden; background: #0f1419; color: #e7ecf1; font-family: system-ui, sans-serif; }
10
+ #bar {
11
+ display: flex; align-items: center; gap: 12px; padding: 8px 14px;
12
+ background: #1a2332; border-bottom: 1px solid #2a3548; font-size: 13px;
13
+ }
14
+ #bar .badge {
15
+ padding: 2px 8px; border-radius: 999px; background: #243044; font-size: 11px;
16
+ }
17
+ #bar .badge.ok { background: #1e4d3a; }
18
+ #canvas-wrap { position: absolute; inset: 40px 0 0 0; overflow: hidden; }
19
+ canvas { display: block; width: 100% !important; height: 100% !important; max-width: 100%; max-height: 100%; }
20
+ #err { color: #f88; padding: 12px; display: none; }
21
+ </style>
22
+ </head>
23
+ <body>
24
+ <div id="bar">
25
+ <strong>robot-arm</strong>
26
+ <span class="badge" id="src">source: —</span>
27
+ <span class="badge" id="rb">ros: —</span>
28
+ <span class="badge" id="unit">unit: __UNIT__</span>
29
+ <span class="badge" id="preset">__PRESET__</span>
30
+ <span style="margin-left:auto;opacity:.7">node __NODE_ID__</span>
31
+ </div>
32
+ <div id="err"></div>
33
+ <div id="canvas-wrap"></div>
34
+ <script>
35
+ window.__ROBOT_ARM__ = {
36
+ nodeId: '__NODE_ID__',
37
+ preset: '__PRESET__',
38
+ urdf: '__URDF__',
39
+ unit: '__UNIT__'
40
+ };
41
+ </script>
42
+ <script type="importmap">
43
+ {
44
+ "imports": {
45
+ "three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
46
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/",
47
+ "three/examples/jsm/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/",
48
+ "urdf-loader": "https://cdn.jsdelivr.net/npm/urdf-loader@0.13.1/src/URDFLoader.js"
49
+ }
50
+ }
51
+ </script>
52
+ <script type="module" src="/robot-arm/static/viewer.js"></script>
53
+ </body>
54
+ </html>
@@ -0,0 +1,195 @@
1
+ import * as THREE from 'three';
2
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
3
+ import URDFLoader from 'urdf-loader';
4
+
5
+ const cfg = window.__ROBOT_ARM__ || {};
6
+ const nodeId = cfg.nodeId;
7
+ const preset = cfg.preset || 'ur5e';
8
+ const urdfFile = cfg.urdf || (preset === 'ur5e-gripper' ? 'ur5e_gripper.urdf' : 'ur5e.urdf');
9
+ const errEl = document.getElementById('err');
10
+ const srcEl = document.getElementById('src');
11
+ const rbEl = document.getElementById('rb');
12
+
13
+ function showErr(msg) {
14
+ errEl.style.display = 'block';
15
+ errEl.textContent = msg;
16
+ }
17
+
18
+ const wrap = document.getElementById('canvas-wrap');
19
+ const scene = new THREE.Scene();
20
+ scene.background = new THREE.Color(0x0f1419);
21
+
22
+ const camera = new THREE.PerspectiveCamera(45, 1, 0.01, 100);
23
+ const viewDir = new THREE.Vector3(0.9, 0.55, 0.9);
24
+
25
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
26
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
27
+ wrap.appendChild(renderer.domElement);
28
+
29
+ const controls = new OrbitControls(camera, renderer.domElement);
30
+ controls.enablePan = false;
31
+ controls.target.set(0, 0, 0);
32
+ controls.update();
33
+
34
+ scene.add(new THREE.AmbientLight(0xffffff, 0.55));
35
+ const dir = new THREE.DirectionalLight(0xffffff, 0.85);
36
+ dir.position.set(2, 3, 1);
37
+ scene.add(dir);
38
+ const grid = new THREE.GridHelper(2, 20, 0x334466, 0x1e2a3a);
39
+ scene.add(grid);
40
+
41
+ let robot = null;
42
+ let poseFramed = false;
43
+
44
+ // Keep the base (robot origin) at the center of the panel and pull the
45
+ // camera back until the whole arm fits the current width and height.
46
+ function frameBase(keepAngle) {
47
+ if (!robot) return;
48
+ const w = wrap.clientWidth;
49
+ const h = wrap.clientHeight;
50
+ if (w < 2 || h < 2) return;
51
+ camera.aspect = w / h;
52
+ renderer.setSize(w, h, false);
53
+ renderer.domElement.style.width = '100%';
54
+ renderer.domElement.style.height = '100%';
55
+ robot.updateMatrixWorld(true);
56
+ const origin = new THREE.Vector3();
57
+ robot.getWorldPosition(origin);
58
+ const box = new THREE.Box3().setFromObject(robot);
59
+ if (box.isEmpty()) return;
60
+ const corner = new THREE.Vector3();
61
+ let radius = 0.25;
62
+ for (const x of [box.min.x, box.max.x]) {
63
+ for (const y of [box.min.y, box.max.y]) {
64
+ for (const z of [box.min.z, box.max.z]) {
65
+ corner.set(x, y, z);
66
+ radius = Math.max(radius, corner.distanceTo(origin));
67
+ }
68
+ }
69
+ }
70
+ const vFov = THREE.MathUtils.degToRad(camera.fov);
71
+ const hFov = 2 * Math.atan(Math.tan(vFov / 2) * camera.aspect);
72
+ const distance = Math.max(radius / Math.tan(vFov / 2), radius / Math.tan(hFov / 2)) * 1.35;
73
+ if (keepAngle) {
74
+ viewDir.copy(camera.position).sub(controls.target);
75
+ if (viewDir.lengthSq() < 1e-8) viewDir.set(0.9, 0.55, 0.9);
76
+ } else {
77
+ viewDir.set(0.9, 0.55, 0.9);
78
+ }
79
+ viewDir.normalize();
80
+ controls.target.copy(origin);
81
+ camera.position.copy(origin).addScaledVector(viewDir, distance);
82
+ camera.near = Math.max(distance / 200, 0.001);
83
+ camera.far = Math.max(distance * 20, 10);
84
+ camera.updateProjectionMatrix();
85
+ const floor = Math.max(radius * 2.4, 0.6);
86
+ grid.scale.set(floor / 2, 1, floor / 2);
87
+ controls.update();
88
+ }
89
+
90
+ function resize() {
91
+ frameBase(true);
92
+ }
93
+ window.addEventListener('resize', resize);
94
+ if (typeof ResizeObserver !== 'undefined') {
95
+ new ResizeObserver(() => frameBase(true)).observe(wrap);
96
+ }
97
+ resize();
98
+
99
+ function animate() {
100
+ requestAnimationFrame(animate);
101
+ controls.update();
102
+ renderer.render(scene, camera);
103
+ }
104
+ animate();
105
+
106
+ function applyJoints(joints) {
107
+ if (!robot || !joints) return;
108
+ for (const [name, value] of Object.entries(joints)) {
109
+ const j = robot.joints && robot.joints[name];
110
+ if (j) {
111
+ try {
112
+ j.setJointValue(value);
113
+ } catch (_) {
114
+ if (typeof j.setJointValue === 'function') j.setJointValue(value);
115
+ }
116
+ }
117
+ }
118
+ if (joints.finger_joint != null && robot.joints && robot.joints.finger_joint_right) {
119
+ try {
120
+ robot.joints.finger_joint_right.setJointValue(joints.finger_joint);
121
+ } catch (_) { /* ignore */ }
122
+ }
123
+ if (!poseFramed) {
124
+ poseFramed = true;
125
+ frameBase(false);
126
+ }
127
+ }
128
+
129
+ async function loadUrdf() {
130
+ const base = `/robot-arm/models/${encodeURIComponent(preset)}/`;
131
+ const url = `${base}${encodeURIComponent(urdfFile)}`;
132
+ const loader = new URDFLoader();
133
+ // Relative mesh paths in URDF are "meshes/foo.stl" → resolve under the preset dir.
134
+ loader.workingPath = base;
135
+ loader.packages = {
136
+ '': base,
137
+ ur5e: base,
138
+ 'ur5e-gripper': base
139
+ };
140
+ return new Promise((resolve, reject) => {
141
+ loader.load(
142
+ url,
143
+ (result) => resolve(result),
144
+ undefined,
145
+ (e) => reject(e)
146
+ );
147
+ });
148
+ }
149
+
150
+ try {
151
+ robot = await loadUrdf();
152
+ // URDF Z-up → Three.js Y-up
153
+ robot.rotation.x = -Math.PI / 2;
154
+ robot.traverse((obj) => {
155
+ if (!obj.isMesh || !obj.material) return;
156
+ const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
157
+ for (const mat of mats) {
158
+ mat.side = THREE.DoubleSide;
159
+ if ('metalness' in mat) {
160
+ mat.metalness = 0.35;
161
+ mat.roughness = 0.45;
162
+ }
163
+ mat.needsUpdate = true;
164
+ }
165
+ });
166
+ scene.add(robot);
167
+ frameBase(false);
168
+ } catch (e) {
169
+ showErr('URDF load failed: ' + (e && e.message ? e.message : String(e)));
170
+ }
171
+
172
+ function onState(data) {
173
+ if (!data) return;
174
+ srcEl.textContent = 'source: ' + (data.source || '—');
175
+ srcEl.classList.toggle('ok', !!data.source);
176
+ rbEl.textContent = 'ros: ' + (data.rosbridge || '—');
177
+ rbEl.classList.toggle('ok', data.rosbridge === 'connected');
178
+ applyJoints(data.joints);
179
+ }
180
+
181
+ const es = new EventSource(`/robot-arm/api/${encodeURIComponent(nodeId)}/state?stream=1`);
182
+ es.onmessage = (ev) => {
183
+ try {
184
+ onState(JSON.parse(ev.data));
185
+ } catch (_) { /* ignore */ }
186
+ };
187
+ es.onerror = () => {
188
+ es.close();
189
+ setInterval(async () => {
190
+ try {
191
+ const r = await fetch(`/robot-arm/api/${encodeURIComponent(nodeId)}/state`);
192
+ onState(await r.json());
193
+ } catch (_) { /* ignore */ }
194
+ }, 200);
195
+ };
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <div class="ui-robot-arm-wrapper">
3
+ <iframe
4
+ v-if="robotId"
5
+ class="ui-robot-arm-frame"
6
+ :src="viewUrl"
7
+ title="Robot arm 3D view"
8
+ allow="fullscreen"
9
+ />
10
+ <div v-else class="ui-robot-arm-empty">
11
+ Select a <strong>robot</strong> node in the widget properties.
12
+ </div>
13
+ </div>
14
+ </template>
15
+
16
+ <script>
17
+ export default {
18
+ name: 'UIRobotArm',
19
+ inject: ['$dataTracker'],
20
+ props: {
21
+ id: { type: String, required: true },
22
+ props: { type: Object, default: () => ({}) },
23
+ state: { type: Object, default: () => ({ enabled: false, visible: false }) }
24
+ },
25
+ computed: {
26
+ robotId () {
27
+ return (this.props && this.props.robot) || ''
28
+ },
29
+ viewUrl () {
30
+ return this.robotId ? `/robot-arm/view/${encodeURIComponent(this.robotId)}` : ''
31
+ }
32
+ },
33
+ created () {
34
+ this.$dataTracker(this.id)
35
+ }
36
+ }
37
+ </script>
38
+
39
+ <style scoped>
40
+ .ui-robot-arm-wrapper {
41
+ width: 100%;
42
+ height: 100%;
43
+ min-height: 280px;
44
+ display: flex;
45
+ flex-direction: column;
46
+ overflow: hidden;
47
+ border-radius: 6px;
48
+ background: #0f1419;
49
+ }
50
+ .ui-robot-arm-frame {
51
+ flex: 1;
52
+ width: 100%;
53
+ height: 100%;
54
+ min-height: 280px;
55
+ border: 0;
56
+ background: #0f1419;
57
+ }
58
+ .ui-robot-arm-empty {
59
+ padding: 16px;
60
+ color: #8b9bb4;
61
+ font-size: 13px;
62
+ }
63
+ </style>
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <div class="ui-robot-controller-wrapper">
3
+ <iframe
4
+ v-if="robotId"
5
+ class="ui-robot-controller-frame"
6
+ :src="pendantUrl"
7
+ title="Robot teach pendant"
8
+ />
9
+ <div v-else class="ui-robot-controller-empty">
10
+ Select a <strong>robot</strong> node in the widget properties.
11
+ </div>
12
+ </div>
13
+ </template>
14
+
15
+ <script>
16
+ export default {
17
+ name: 'UIRobotController',
18
+ inject: ['$dataTracker'],
19
+ props: {
20
+ id: { type: String, required: true },
21
+ props: { type: Object, default: () => ({}) },
22
+ state: { type: Object, default: () => ({ enabled: false, visible: false }) }
23
+ },
24
+ computed: {
25
+ robotId () {
26
+ return (this.props && this.props.robot) || ''
27
+ },
28
+ pendantUrl () {
29
+ return this.robotId ? `/robot-arm/pendant/${encodeURIComponent(this.robotId)}` : ''
30
+ }
31
+ },
32
+ created () {
33
+ this.$dataTracker(this.id)
34
+ }
35
+ }
36
+ </script>
37
+
38
+ <style scoped>
39
+ .ui-robot-controller-wrapper {
40
+ width: 100%;
41
+ height: 100%;
42
+ min-height: 280px;
43
+ display: flex;
44
+ flex-direction: column;
45
+ overflow: hidden;
46
+ border-radius: 6px;
47
+ background: #0f1419;
48
+ }
49
+ .ui-robot-controller-frame {
50
+ flex: 1;
51
+ width: 100%;
52
+ height: 100%;
53
+ min-height: 280px;
54
+ border: 0;
55
+ background: #0f1419;
56
+ }
57
+ .ui-robot-controller-empty {
58
+ padding: 16px;
59
+ color: #8b9bb4;
60
+ font-size: 13px;
61
+ }
62
+ </style>
package/ui/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { default as UIRobotArm } from './components/UIRobotArm.vue'
2
+ export { default as UIRobotController } from './components/UIRobotController.vue'