@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.
@@ -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 &mdash; 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 &mdash; 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 &mdash; 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 &mdash; 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 &mdash; 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> &mdash; basic-type wrappers
66
+ (String, Real, Boolean, ...)</li>
67
+ <li><code>SiLAService.proto</code> &mdash; 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 &mdash; 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 &mdash; 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_&lt;Property&gt;_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 &mdash; only unobservable (single read) properties
123
+ work today. Observable properties would need server-streaming
124
+ subscription handling.</p>
125
+ </script>