@synefex/node-red-sila2 0.1.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 +36 -0
- package/LICENSE +201 -0
- package/NOTICE +24 -0
- package/README.md +119 -0
- package/package.json +56 -0
- package/protos/SiLAFramework.proto +132 -0
- package/protos/SiLAService.proto +81 -0
- package/src/call-command/sila-call-command.html +137 -0
- package/src/call-command/sila-call-command.js +110 -0
- package/src/connection/sila-connection.html +101 -0
- package/src/connection/sila-connection.js +148 -0
- package/src/get-property/sila-get-property.html +125 -0
- package/src/get-property/sila-get-property.js +114 -0
- package/src/lib/client.js +216 -0
- package/src/lib/picker/picker.js +174 -0
- package/src/lib/unwrap.js +35 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType("sila-call-command", {
|
|
3
|
+
// Space (not '-') in the category: Node-RED 4 splits ASCII-dashed
|
|
4
|
+
// categories at the first '-' to form a collapsible parent group,
|
|
5
|
+
// so "Synefex-SiLA-2" would share a section with "Synefex-LADS".
|
|
6
|
+
// The space survives Node-RED's escape-then-display round-trip
|
|
7
|
+
// (space → '_' in IDs, '_' → space in the displayed label) and
|
|
8
|
+
// doesn't trigger the split, giving us a standalone section.
|
|
9
|
+
category: "Synefex SiLA 2",
|
|
10
|
+
color: "#A6BBCF",
|
|
11
|
+
defaults: {
|
|
12
|
+
name: { value: "" },
|
|
13
|
+
connection: { value: "", type: "sila-connection", required: true },
|
|
14
|
+
feature: { value: "" },
|
|
15
|
+
featureType: { value: "str" },
|
|
16
|
+
command: { value: "" },
|
|
17
|
+
commandType: { value: "str" },
|
|
18
|
+
params: { value: "{}" },
|
|
19
|
+
paramsType: { value: "json" },
|
|
20
|
+
outputProperty: { value: "payload" },
|
|
21
|
+
outputPropertyType: { value: "msg" }
|
|
22
|
+
},
|
|
23
|
+
inputs: 1,
|
|
24
|
+
outputs: 1,
|
|
25
|
+
icon: "font-awesome/fa-bolt",
|
|
26
|
+
paletteLabel: "SiLA 2 call",
|
|
27
|
+
label: function () {
|
|
28
|
+
if (this.name) return this.name;
|
|
29
|
+
if (this.commandType === "str" && this.command) return "call " + this.command;
|
|
30
|
+
return "SiLA 2 call";
|
|
31
|
+
},
|
|
32
|
+
oneditprepare: function () {
|
|
33
|
+
$("#node-input-feature").typedInput({
|
|
34
|
+
default: this.featureType || "str",
|
|
35
|
+
typeField: $("#node-input-featureType"),
|
|
36
|
+
types: ["str", "msg", "flow", "global", "env"]
|
|
37
|
+
});
|
|
38
|
+
$("#node-input-command").typedInput({
|
|
39
|
+
default: this.commandType || "str",
|
|
40
|
+
typeField: $("#node-input-commandType"),
|
|
41
|
+
types: ["str", "msg", "flow", "global", "env"]
|
|
42
|
+
});
|
|
43
|
+
$("#node-input-params").typedInput({
|
|
44
|
+
default: this.paramsType || "json",
|
|
45
|
+
typeField: $("#node-input-paramsType"),
|
|
46
|
+
types: ["json", "msg", "flow", "global"]
|
|
47
|
+
});
|
|
48
|
+
$("#node-input-outputProperty").typedInput({
|
|
49
|
+
default: this.outputPropertyType || "msg",
|
|
50
|
+
typeField: $("#node-input-outputPropertyType"),
|
|
51
|
+
types: ["msg", "flow", "global"]
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Picker: cascading Feature → Command dropdowns. Writes the chosen
|
|
55
|
+
// values into the manual fields; users can also still type.
|
|
56
|
+
if (window.SilaPicker) {
|
|
57
|
+
window.SilaPicker.attach({
|
|
58
|
+
kind: "command",
|
|
59
|
+
connectionFieldId: "node-input-connection",
|
|
60
|
+
featureFieldId: "node-input-feature",
|
|
61
|
+
methodFieldId: "node-input-command",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<script type="text/html" data-template-name="sila-call-command">
|
|
69
|
+
<div class="form-row">
|
|
70
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
71
|
+
<input type="text" id="node-input-name">
|
|
72
|
+
</div>
|
|
73
|
+
<div class="form-row">
|
|
74
|
+
<label for="node-input-connection"><i class="fa fa-server"></i> Connection</label>
|
|
75
|
+
<input type="text" id="node-input-connection">
|
|
76
|
+
</div>
|
|
77
|
+
<div class="form-row">
|
|
78
|
+
<label for="node-input-feature"><i class="fa fa-cube"></i> Feature</label>
|
|
79
|
+
<input type="text" id="node-input-feature" placeholder="MettlerToledoAX205Controller">
|
|
80
|
+
<input type="hidden" id="node-input-featureType">
|
|
81
|
+
</div>
|
|
82
|
+
<div class="form-row">
|
|
83
|
+
<label for="node-input-command"><i class="fa fa-bolt"></i> Command</label>
|
|
84
|
+
<input type="text" id="node-input-command" placeholder="GetStableWeight">
|
|
85
|
+
<input type="hidden" id="node-input-commandType">
|
|
86
|
+
</div>
|
|
87
|
+
<div class="form-row">
|
|
88
|
+
<label for="node-input-params"><i class="fa fa-list"></i> Parameters</label>
|
|
89
|
+
<input type="text" id="node-input-params">
|
|
90
|
+
<input type="hidden" id="node-input-paramsType">
|
|
91
|
+
</div>
|
|
92
|
+
<div class="form-row">
|
|
93
|
+
<label for="node-input-outputProperty"><i class="fa fa-arrow-right"></i> Output to</label>
|
|
94
|
+
<input type="text" id="node-input-outputProperty">
|
|
95
|
+
<input type="hidden" id="node-input-outputPropertyType">
|
|
96
|
+
</div>
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<script type="text/html" data-help-name="sila-call-command">
|
|
100
|
+
<p>Call an unobservable SiLA 2 command on a feature.</p>
|
|
101
|
+
|
|
102
|
+
<h3>Configuration</h3>
|
|
103
|
+
<dl class="message-properties">
|
|
104
|
+
<dt>Feature <span class="property-type">string</span></dt>
|
|
105
|
+
<dd>The feature identifier — short name (e.g.
|
|
106
|
+
<code>MettlerToledoAX205Controller</code>) or full proto-qualified
|
|
107
|
+
name. The connection node resolves short names via gRPC reflection
|
|
108
|
+
(case-insensitive match against the tail segment of the proto service
|
|
109
|
+
name).</dd>
|
|
110
|
+
<dt>Command <span class="property-type">string</span></dt>
|
|
111
|
+
<dd>The command name as declared in the feature (e.g.
|
|
112
|
+
<code>GetStableWeight</code>, <code>Tare</code>, <code>CloseDoor</code>).</dd>
|
|
113
|
+
<dt>Parameters <span class="property-type">object</span></dt>
|
|
114
|
+
<dd>Plain object encoded into the command's <code>_Parameters</code>
|
|
115
|
+
message. Use <code>{}</code> for parameter-less commands. Field
|
|
116
|
+
values can be raw natives — SiLA 2 basic-type wrapping is
|
|
117
|
+
applied at encode time via the protobufjs schema fetched by
|
|
118
|
+
reflection.</dd>
|
|
119
|
+
</dl>
|
|
120
|
+
|
|
121
|
+
<h3>Output</h3>
|
|
122
|
+
<p>The decoded <code>_Responses</code> message with SiLA 2 basic-type
|
|
123
|
+
wrappers (<code>{ value: ... }</code>) automatically unwrapped. Example
|
|
124
|
+
for <code>GetStableWeight</code>:</p>
|
|
125
|
+
<pre>{ Status: "S S", WeightValue: 82.6824, Unit: "g" }</pre>
|
|
126
|
+
|
|
127
|
+
<h3>Errors</h3>
|
|
128
|
+
<p>gRPC errors propagate via <code>done(err)</code>. SiLA 2 framework
|
|
129
|
+
errors (validation, undefined execution, defined errors) currently
|
|
130
|
+
surface as generic gRPC errors — full <code>sila-error-bin</code>
|
|
131
|
+
trailer decoding is on the roadmap.</p>
|
|
132
|
+
|
|
133
|
+
<h3>Observable commands</h3>
|
|
134
|
+
<p>Not yet supported — only unobservable (single round-trip)
|
|
135
|
+
commands work today. Observable commands need execution-UUID tracking
|
|
136
|
+
and server-streaming subscriptions.</p>
|
|
137
|
+
</script>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
module.exports = function (RED) {
|
|
4
|
+
function SilaCallCommandNode(config) {
|
|
5
|
+
RED.nodes.createNode(this, config);
|
|
6
|
+
const node = this;
|
|
7
|
+
|
|
8
|
+
node.connection = RED.nodes.getNode(config.connection);
|
|
9
|
+
node.feature = config.feature || "";
|
|
10
|
+
node.featureType = config.featureType || "str";
|
|
11
|
+
node.command = config.command || "";
|
|
12
|
+
node.commandType = config.commandType || "str";
|
|
13
|
+
node.params = config.params || "{}";
|
|
14
|
+
node.paramsType = config.paramsType || "json";
|
|
15
|
+
node.outputProperty = config.outputProperty || "payload";
|
|
16
|
+
node.outputPropertyType = config.outputPropertyType || "msg";
|
|
17
|
+
|
|
18
|
+
if (!node.connection) {
|
|
19
|
+
node.status({ fill: "red", shape: "ring", text: "no connection" });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function applyConnStatus(status) {
|
|
24
|
+
switch (status.state) {
|
|
25
|
+
case "ok":
|
|
26
|
+
node.status({ fill: "green", shape: "dot", text: status.detail || "ok" });
|
|
27
|
+
break;
|
|
28
|
+
case "ready":
|
|
29
|
+
node.status({ fill: "grey", shape: "ring", text: "ready" });
|
|
30
|
+
break;
|
|
31
|
+
case "error":
|
|
32
|
+
node.status({ fill: "red", shape: "ring", text: status.detail || "error" });
|
|
33
|
+
break;
|
|
34
|
+
default:
|
|
35
|
+
node.status({ fill: "grey", shape: "ring", text: status.state || "" });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const unsub = node.connection.onStatus(applyConnStatus);
|
|
39
|
+
|
|
40
|
+
node.on("input", async (msg, send, done) => {
|
|
41
|
+
let feature, command, params;
|
|
42
|
+
try {
|
|
43
|
+
feature = RED.util.evaluateNodeProperty(node.feature, node.featureType, node, msg);
|
|
44
|
+
command = RED.util.evaluateNodeProperty(node.command, node.commandType, node, msg);
|
|
45
|
+
params = RED.util.evaluateNodeProperty(node.params, node.paramsType, node, msg);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (done) done(err); else node.error(err, msg);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof feature !== "string" || !feature) {
|
|
52
|
+
const err = new Error("SiLA call: feature name is missing");
|
|
53
|
+
if (done) done(err); else node.error(err, msg);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (typeof command !== "string" || !command) {
|
|
57
|
+
const err = new Error("SiLA call: command name is missing");
|
|
58
|
+
if (done) done(err); else node.error(err, msg);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// typed-input "json" already parses; msg/flow/global may pass through a
|
|
63
|
+
// string by accident — be tolerant. null → empty params.
|
|
64
|
+
if (params == null) params = {};
|
|
65
|
+
if (typeof params === "string") {
|
|
66
|
+
try {
|
|
67
|
+
params = JSON.parse(params);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
const err = new Error(`SiLA call: params is not valid JSON: ${e.message}`);
|
|
70
|
+
if (done) done(err); else node.error(err, msg);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let resp;
|
|
76
|
+
try {
|
|
77
|
+
resp = await node.connection.callUnary(feature, command, params);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
node.status({ fill: "red", shape: "ring", text: "call failed" });
|
|
80
|
+
if (done) done(err); else node.error(err, msg);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
if (node.outputPropertyType === "msg") {
|
|
86
|
+
RED.util.setMessageProperty(msg, node.outputProperty, resp, true);
|
|
87
|
+
} else if (
|
|
88
|
+
node.outputPropertyType === "flow" ||
|
|
89
|
+
node.outputPropertyType === "global"
|
|
90
|
+
) {
|
|
91
|
+
node.context()[node.outputPropertyType].set(node.outputProperty, resp);
|
|
92
|
+
} else {
|
|
93
|
+
RED.util.setMessageProperty(msg, node.outputProperty, resp, true);
|
|
94
|
+
}
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (done) done(err); else node.error(err, msg);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
send(msg);
|
|
101
|
+
if (done) done();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
node.on("close", () => {
|
|
105
|
+
try { unsub(); } catch (_) { /* ignore */ }
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
RED.nodes.registerType("sila-call-command", SilaCallCommandNode);
|
|
110
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<!-- Browser-side SiLA 2 picker widget. Served as a static asset by the
|
|
2
|
+
connection node's admin route; loaded here once so it's available on
|
|
3
|
+
window.SilaPicker for every action node's editor dialog. -->
|
|
4
|
+
<script type="text/javascript" src="sila/picker.js"></script>
|
|
5
|
+
|
|
6
|
+
<script type="text/javascript">
|
|
7
|
+
RED.nodes.registerType("sila-connection", {
|
|
8
|
+
category: "config",
|
|
9
|
+
defaults: {
|
|
10
|
+
name: { value: "" },
|
|
11
|
+
host: { value: "127.0.0.1", required: true },
|
|
12
|
+
port: { value: 50052, required: true, validate: RED.validators.number() },
|
|
13
|
+
insecure: { value: true },
|
|
14
|
+
extraProtoDirs: { value: "" },
|
|
15
|
+
extraProtoFiles: { value: "" }
|
|
16
|
+
},
|
|
17
|
+
label: function () {
|
|
18
|
+
const target = (this.host && this.port) ? `${this.host}:${this.port}` : "";
|
|
19
|
+
return this.name || target || "SiLA 2 connection";
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<script type="text/html" data-template-name="sila-connection">
|
|
25
|
+
<div class="form-row">
|
|
26
|
+
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
27
|
+
<input type="text" id="node-config-input-name" placeholder="e.g. AX205 balance">
|
|
28
|
+
</div>
|
|
29
|
+
<div class="form-row">
|
|
30
|
+
<label for="node-config-input-host"><i class="fa fa-globe"></i> Host</label>
|
|
31
|
+
<input type="text" id="node-config-input-host" placeholder="192.168.8.225">
|
|
32
|
+
</div>
|
|
33
|
+
<div class="form-row">
|
|
34
|
+
<label for="node-config-input-port"><i class="fa fa-plug"></i> Port</label>
|
|
35
|
+
<input type="number" id="node-config-input-port" placeholder="50052">
|
|
36
|
+
</div>
|
|
37
|
+
<div class="form-row">
|
|
38
|
+
<label for="node-config-input-insecure" style="width:auto">
|
|
39
|
+
<input type="checkbox" id="node-config-input-insecure" style="display:inline-block; width:auto; vertical-align:middle">
|
|
40
|
+
Insecure (no TLS — lab/dev mode)
|
|
41
|
+
</label>
|
|
42
|
+
</div>
|
|
43
|
+
<hr>
|
|
44
|
+
<div class="form-row">
|
|
45
|
+
<label for="node-config-input-extraProtoDirs"><i class="fa fa-folder-open"></i> Extra proto dirs</label>
|
|
46
|
+
<input type="text" id="node-config-input-extraProtoDirs"
|
|
47
|
+
placeholder="/home/user/sila-protos, /opt/protos">
|
|
48
|
+
</div>
|
|
49
|
+
<div class="form-row">
|
|
50
|
+
<label for="node-config-input-extraProtoFiles"><i class="fa fa-file-code-o"></i> Extra proto files</label>
|
|
51
|
+
<input type="text" id="node-config-input-extraProtoFiles"
|
|
52
|
+
placeholder="/path/to/MyFeature.proto">
|
|
53
|
+
</div>
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<script type="text/html" data-help-name="sila-connection">
|
|
57
|
+
<p>gRPC connection to a SiLA 2 server. One config node per server
|
|
58
|
+
endpoint. Owns a long-lived gRPC channel and a feature registry loaded
|
|
59
|
+
from <code>.proto</code> files, shared by every <code>sila-*</code>
|
|
60
|
+
action node that references it.</p>
|
|
61
|
+
|
|
62
|
+
<h3>Bundled features</h3>
|
|
63
|
+
<p>The package always loads:</p>
|
|
64
|
+
<ul>
|
|
65
|
+
<li><code>SiLAFramework.proto</code> — basic-type wrappers
|
|
66
|
+
(String, Real, Boolean, ...)</li>
|
|
67
|
+
<li><code>SiLAService.proto</code> — the mandatory core feature
|
|
68
|
+
every SiLA 2 server implements (ServerName, ImplementedFeatures, ...)</li>
|
|
69
|
+
</ul>
|
|
70
|
+
<p>That's enough to query any SiLA 2 server's identity surface
|
|
71
|
+
(`SiLAService` properties + commands) out of the box.</p>
|
|
72
|
+
|
|
73
|
+
<h3>Adding more features</h3>
|
|
74
|
+
<p>For domain features (balances, liquid handlers, etc.), you supply
|
|
75
|
+
their feature-specific <code>.proto</code> files. SiLA 2 servers
|
|
76
|
+
generate <code>.proto</code> from FDL XML; if your server runs the
|
|
77
|
+
<code>sila2</code> Python reference implementation, the generated
|
|
78
|
+
files live inside the running container and can be copied out with a
|
|
79
|
+
single shell command.</p>
|
|
80
|
+
<p>Once you have the <code>.proto</code> files, drop them in a
|
|
81
|
+
directory and point <b>Extra proto dirs</b> (or <b>Extra proto files</b>)
|
|
82
|
+
at the location. The lists are comma-separated; <code>.proto</code>
|
|
83
|
+
files in any listed directory are picked up automatically.</p>
|
|
84
|
+
|
|
85
|
+
<h3>Reflection</h3>
|
|
86
|
+
<p>Earlier versions of this package used gRPC reflection to discover
|
|
87
|
+
features at runtime. The Python <code>sila2</code> reference server
|
|
88
|
+
enables reflection by default, but our lab server has it disabled, and
|
|
89
|
+
Java/C# servers may not support it. Static proto loading is universal.</p>
|
|
90
|
+
|
|
91
|
+
<h3>TLS</h3>
|
|
92
|
+
<p>For development against an insecure server, leave <b>Insecure</b>
|
|
93
|
+
enabled. Production SiLA 2 servers run TLS with a self-signed cert
|
|
94
|
+
whose CN encodes the server UUID — pinned-cert TLS is not in
|
|
95
|
+
this phase of the package.</p>
|
|
96
|
+
|
|
97
|
+
<h3>Lifecycle</h3>
|
|
98
|
+
<p>Protos load on first call (no I/O at deploy). Per-service grpc
|
|
99
|
+
client instances are created lazily and reused across calls. Redeploy
|
|
100
|
+
to flush the cache.</p>
|
|
101
|
+
</script>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const grpc = require("@grpc/grpc-js");
|
|
6
|
+
const { createSilaClient } = require("../lib/client");
|
|
7
|
+
|
|
8
|
+
// Read once at module load — served by the picker.js admin route below.
|
|
9
|
+
const PICKER_JS = fs.readFileSync(
|
|
10
|
+
path.join(__dirname, "..", "lib", "picker", "picker.js"),
|
|
11
|
+
"utf8",
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
module.exports = function (RED) {
|
|
15
|
+
function SilaConnectionNode(config) {
|
|
16
|
+
RED.nodes.createNode(this, config);
|
|
17
|
+
const node = this;
|
|
18
|
+
|
|
19
|
+
node.host = (config.host || "").trim();
|
|
20
|
+
node.port = parseInt(config.port, 10) || 50052;
|
|
21
|
+
node.insecure = config.insecure !== false; // default true (lab/dev)
|
|
22
|
+
|
|
23
|
+
// Comma-separated list of extra proto dirs and/or .proto files. The
|
|
24
|
+
// bundled protos (SiLAFramework, SiLAService, sample AX205Controller)
|
|
25
|
+
// are always loaded; this lets the user point at additional features
|
|
26
|
+
// extracted from their server (e.g. via sila-handoff/examples/extract_protos.sh).
|
|
27
|
+
node.extraProtoDirs = parseList(config.extraProtoDirs);
|
|
28
|
+
node.extraProtoFiles = parseList(config.extraProtoFiles);
|
|
29
|
+
|
|
30
|
+
/** @type {ReturnType<typeof createSilaClient>|null} */
|
|
31
|
+
let client = null;
|
|
32
|
+
|
|
33
|
+
const statusListeners = new Set();
|
|
34
|
+
let lastStatus = { state: "idle", detail: "", ts: Date.now() };
|
|
35
|
+
|
|
36
|
+
function publishStatus(state, detail) {
|
|
37
|
+
lastStatus = { state, detail: detail || "", ts: Date.now() };
|
|
38
|
+
for (const fn of statusListeners) {
|
|
39
|
+
try { fn(lastStatus); } catch (_) { /* ignore */ }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Action nodes subscribe with this; returns an unsubscribe fn. */
|
|
44
|
+
node.onStatus = function (fn) {
|
|
45
|
+
statusListeners.add(fn);
|
|
46
|
+
try { fn(lastStatus); } catch (_) { /* ignore */ }
|
|
47
|
+
return () => statusListeners.delete(fn);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function buildClient() {
|
|
51
|
+
if (!node.host) throw new Error("SiLA connection: host not configured");
|
|
52
|
+
const target = `${node.host}:${node.port}`;
|
|
53
|
+
const creds = node.insecure
|
|
54
|
+
? grpc.credentials.createInsecure()
|
|
55
|
+
: grpc.credentials.createSsl(); // TLS w/o pinned roots
|
|
56
|
+
const c = createSilaClient({
|
|
57
|
+
target,
|
|
58
|
+
credentials: creds,
|
|
59
|
+
extraProtoDirs: node.extraProtoDirs,
|
|
60
|
+
extraProtoFiles: node.extraProtoFiles,
|
|
61
|
+
});
|
|
62
|
+
publishStatus("ready", `${target} (${c.listServices().length} svc loaded)`);
|
|
63
|
+
return c;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Lazy: build the client on first call. Loading protos is synchronous
|
|
68
|
+
* and fast (KB-sized files), but no socket opens until the first call.
|
|
69
|
+
*/
|
|
70
|
+
node.getClient = function () {
|
|
71
|
+
if (!client) client = buildClient();
|
|
72
|
+
return client;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convenience wrapper for action nodes: lazy init + status broadcast.
|
|
77
|
+
*/
|
|
78
|
+
node.callUnary = async function (featureName, methodName, params, metadata) {
|
|
79
|
+
const c = node.getClient();
|
|
80
|
+
try {
|
|
81
|
+
const resp = await c.callUnary(featureName, methodName, params, metadata);
|
|
82
|
+
publishStatus("ok", `${featureName}.${methodName}`);
|
|
83
|
+
return resp;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
publishStatus("error", err.message || String(err));
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** For diagnostics / future picker UI. */
|
|
91
|
+
node.listServices = function () {
|
|
92
|
+
return node.getClient().listServices();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Editor picker: list loaded features with classified methods. */
|
|
96
|
+
node.listFeaturesForPicker = function () {
|
|
97
|
+
return node.getClient().listFeaturesForPicker();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
node.on("close", function (done) {
|
|
101
|
+
try { if (client) client.close(); } catch (_) { /* ignore */ }
|
|
102
|
+
client = null;
|
|
103
|
+
if (typeof done === "function") done();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseList(s) {
|
|
108
|
+
if (!s) return [];
|
|
109
|
+
return String(s)
|
|
110
|
+
.split(",")
|
|
111
|
+
.map((x) => x.trim())
|
|
112
|
+
.filter(Boolean);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
RED.nodes.registerType("sila-connection", SilaConnectionNode);
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Editor-side picker support: admin HTTP endpoints. Routes are scoped
|
|
119
|
+
// under /sila/conn/:id/... to mirror the LADS pattern and avoid clashes.
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
function picker(handler) {
|
|
123
|
+
return async (req, res) => {
|
|
124
|
+
const node = RED.nodes.getNode(req.params.id);
|
|
125
|
+
if (!node || typeof node.listFeaturesForPicker !== "function") {
|
|
126
|
+
return res.status(404).json({ error: "sila-connection not found: " + req.params.id });
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const data = await handler(node, req);
|
|
130
|
+
res.json(data);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
res.status(500).json({ error: err.message });
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
RED.httpAdmin.get(
|
|
138
|
+
"/sila/conn/:id/features",
|
|
139
|
+
RED.auth.needsPermission("flows.read"),
|
|
140
|
+
picker((node) => node.listFeaturesForPicker()),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Serve the picker.js asset for the editor. Static JS — sensitive data
|
|
144
|
+
// lives behind the /features endpoint which already requires flows.read.
|
|
145
|
+
RED.httpAdmin.get("/sila/picker.js", (req, res) => {
|
|
146
|
+
res.type("application/javascript").send(PICKER_JS);
|
|
147
|
+
});
|
|
148
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType("sila-get-property", {
|
|
3
|
+
// See sila-call-command.html for why this is a space, not '-'.
|
|
4
|
+
category: "Synefex SiLA 2",
|
|
5
|
+
color: "#A6BBCF",
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: "" },
|
|
8
|
+
connection: { value: "", type: "sila-connection", required: true },
|
|
9
|
+
feature: { value: "" },
|
|
10
|
+
featureType: { value: "str" },
|
|
11
|
+
property: { value: "" },
|
|
12
|
+
propertyType: { value: "str" },
|
|
13
|
+
outputProperty: { value: "payload" },
|
|
14
|
+
outputPropertyType: { value: "msg" },
|
|
15
|
+
unwrapSingle: { value: true }
|
|
16
|
+
},
|
|
17
|
+
inputs: 1,
|
|
18
|
+
outputs: 1,
|
|
19
|
+
icon: "font-awesome/fa-eye",
|
|
20
|
+
paletteLabel: "SiLA 2 get",
|
|
21
|
+
label: function () {
|
|
22
|
+
if (this.name) return this.name;
|
|
23
|
+
if (this.propertyType === "str" && this.property) return "get " + this.property;
|
|
24
|
+
return "SiLA 2 get";
|
|
25
|
+
},
|
|
26
|
+
oneditprepare: function () {
|
|
27
|
+
$("#node-input-feature").typedInput({
|
|
28
|
+
default: this.featureType || "str",
|
|
29
|
+
typeField: $("#node-input-featureType"),
|
|
30
|
+
types: ["str", "msg", "flow", "global", "env"]
|
|
31
|
+
});
|
|
32
|
+
$("#node-input-property").typedInput({
|
|
33
|
+
default: this.propertyType || "str",
|
|
34
|
+
typeField: $("#node-input-propertyType"),
|
|
35
|
+
types: ["str", "msg", "flow", "global", "env"]
|
|
36
|
+
});
|
|
37
|
+
$("#node-input-outputProperty").typedInput({
|
|
38
|
+
default: this.outputPropertyType || "msg",
|
|
39
|
+
typeField: $("#node-input-outputPropertyType"),
|
|
40
|
+
types: ["msg", "flow", "global"]
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Picker: cascading Feature → Property dropdowns. Properties are
|
|
44
|
+
// surfaced by their bare name (e.g. "ServerName"), not by the wire
|
|
45
|
+
// method name "Get_ServerName" — the node prepends Get_ at runtime.
|
|
46
|
+
if (window.SilaPicker) {
|
|
47
|
+
window.SilaPicker.attach({
|
|
48
|
+
kind: "property",
|
|
49
|
+
connectionFieldId: "node-input-connection",
|
|
50
|
+
featureFieldId: "node-input-feature",
|
|
51
|
+
methodFieldId: "node-input-property",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<script type="text/html" data-template-name="sila-get-property">
|
|
59
|
+
<div class="form-row">
|
|
60
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
61
|
+
<input type="text" id="node-input-name">
|
|
62
|
+
</div>
|
|
63
|
+
<div class="form-row">
|
|
64
|
+
<label for="node-input-connection"><i class="fa fa-server"></i> Connection</label>
|
|
65
|
+
<input type="text" id="node-input-connection">
|
|
66
|
+
</div>
|
|
67
|
+
<div class="form-row">
|
|
68
|
+
<label for="node-input-feature"><i class="fa fa-cube"></i> Feature</label>
|
|
69
|
+
<input type="text" id="node-input-feature" placeholder="SiLAService">
|
|
70
|
+
<input type="hidden" id="node-input-featureType">
|
|
71
|
+
</div>
|
|
72
|
+
<div class="form-row">
|
|
73
|
+
<label for="node-input-property"><i class="fa fa-eye"></i> Property</label>
|
|
74
|
+
<input type="text" id="node-input-property" placeholder="ServerName">
|
|
75
|
+
<input type="hidden" id="node-input-propertyType">
|
|
76
|
+
</div>
|
|
77
|
+
<div class="form-row">
|
|
78
|
+
<label for="node-input-outputProperty"><i class="fa fa-arrow-right"></i> Output to</label>
|
|
79
|
+
<input type="text" id="node-input-outputProperty">
|
|
80
|
+
<input type="hidden" id="node-input-outputPropertyType">
|
|
81
|
+
</div>
|
|
82
|
+
<div class="form-row">
|
|
83
|
+
<label for="node-input-unwrapSingle" style="width:auto">
|
|
84
|
+
<input type="checkbox" id="node-input-unwrapSingle" style="display:inline-block; width:auto; vertical-align:middle">
|
|
85
|
+
Unwrap single-field response (drop the property-named wrapper)
|
|
86
|
+
</label>
|
|
87
|
+
</div>
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<script type="text/html" data-help-name="sila-get-property">
|
|
91
|
+
<p>Get the current value of an unobservable SiLA 2 property.</p>
|
|
92
|
+
|
|
93
|
+
<h3>Configuration</h3>
|
|
94
|
+
<dl class="message-properties">
|
|
95
|
+
<dt>Feature <span class="property-type">string</span></dt>
|
|
96
|
+
<dd>The feature identifier (short name or full proto-qualified
|
|
97
|
+
name).</dd>
|
|
98
|
+
<dt>Property <span class="property-type">string</span></dt>
|
|
99
|
+
<dd>The property name as declared on the feature (e.g.
|
|
100
|
+
<code>ServerName</code>, <code>ImplementedFeatures</code>). The node
|
|
101
|
+
prepends <code>Get_</code> automatically — if you provide
|
|
102
|
+
<code>Get_ServerName</code> it's used verbatim.</dd>
|
|
103
|
+
<dt>Unwrap single-field response <span class="property-type">boolean</span></dt>
|
|
104
|
+
<dd>When the <code>Get_<Property>_Responses</code> message has
|
|
105
|
+
one field (the typical case), unwrap it so <code>payload</code> is
|
|
106
|
+
the value directly. Disable to receive the wrapper object.</dd>
|
|
107
|
+
</dl>
|
|
108
|
+
|
|
109
|
+
<h3>Output</h3>
|
|
110
|
+
<p>The unwrapped property value. Examples:</p>
|
|
111
|
+
<pre>SiLAService.ServerName
|
|
112
|
+
→ "MettlerToledoAX205SiLAServer"
|
|
113
|
+
|
|
114
|
+
SiLAService.ImplementedFeatures
|
|
115
|
+
→ [
|
|
116
|
+
"org.silastandard/core/SiLAService/v1",
|
|
117
|
+
"master.thesis/weighing/MettlerToledoAX205Controller/v1",
|
|
118
|
+
"master.thesis/weighing/MettlerToledoAX205Metadata/v1"
|
|
119
|
+
]</pre>
|
|
120
|
+
|
|
121
|
+
<h3>Observable properties</h3>
|
|
122
|
+
<p>Not yet supported — only unobservable (single read) properties
|
|
123
|
+
work today. Observable properties would need server-streaming
|
|
124
|
+
subscription handling.</p>
|
|
125
|
+
</script>
|