changed rule adding logic

This commit is contained in:
Niko Vujić
2025-12-31 10:41:14 +01:00
parent 7c9c1ddf3a
commit 84797d50c4
4 changed files with 135 additions and 179 deletions

View File

@@ -5,7 +5,7 @@ RED.nodes.registerType('xilica-listen', {
defaults: { defaults: {
name: { value: "" }, name: { value: "" },
connection: { value: "", type: "xilica-connection" }, connection: { value: "", type: "xilica-connection" },
rules: { value: "[]" } rules: { value: [] }
}, },
inputs: 1, inputs: 1,
outputs: 1, outputs: 1,
@@ -18,10 +18,15 @@ RED.nodes.registerType('xilica-listen', {
}, },
oneditprepare: function () { oneditprepare: function () {
var node = this; var node = this;
var rules; var rules = node.rules || [];
try { if (typeof rules === "string") {
rules = node.rules ? JSON.parse(node.rules) : []; try {
} catch (e) { rules = JSON.parse(rules);
} catch (e) {
rules = [];
}
}
if (!Array.isArray(rules)) {
rules = []; rules = [];
} }
@@ -113,7 +118,7 @@ RED.nodes.registerType('xilica-listen', {
rules.push(rule); rules.push(rule);
}); });
this.rules = JSON.stringify(rules); this.rules = rules;
} }
}); });
</script> </script>

View File

@@ -54,15 +54,15 @@ module.exports = function (RED) {
RED.nodes.createNode(this, config); RED.nodes.createNode(this, config);
const node = this; const node = this;
let storedRules = config.rules; let initialRules = config.rules;
if (typeof storedRules === "string") { if (typeof initialRules === "string") {
try { try {
storedRules = JSON.parse(storedRules); initialRules = JSON.parse(initialRules);
} catch (e) { } catch (e) {
storedRules = []; initialRules = [];
} }
} }
node.rules = Array.isArray(storedRules) ? storedRules : []; node.rules = Array.isArray(initialRules) ? initialRules : [];
node.on("input", (msg, send, done) => { node.on("input", (msg, send, done) => {
send = send =

View File

@@ -6,7 +6,10 @@ RED.nodes.registerType('xilica-subscribe', {
name: { value: "" }, name: { value: "" },
action: { value: "subscribe" }, action: { value: "subscribe" },
connection: { value: "", type: "xilica-connection" }, connection: { value: "", type: "xilica-connection" },
rules: { value: "[]" }, mode: { value: "list" },
targets: { value: "" },
startIndex: { value: 1 },
endIndex: { value: 1 },
transport: { value: "TCP" } transport: { value: "TCP" }
}, },
inputs: 1, inputs: 1,
@@ -18,105 +21,6 @@ RED.nodes.registerType('xilica-subscribe', {
return this.name; return this.name;
} }
return "xilica " + act; return "xilica " + act;
},
oneditprepare: function () {
var node = this;
var rules;
try {
rules = node.rules ? JSON.parse(node.rules) : [];
} catch (e) {
rules = [];
}
$("#node-input-rules").editableList({
addItem: function (container, index, rule) {
rule = rule || {};
var row = $('<div/>').appendTo(container);
var typeField = $('<select/>', {
class: "node-input-rule-type",
style: "width: 120px; margin-right: 5px;"
}).appendTo(row);
$('<option/>', { value: "eq", text: "==" }).appendTo(typeField);
$('<option/>', { value: "idx", text: "Indexed" }).appendTo(typeField);
var valueField = $('<input/>', {
class: "node-input-rule-value",
type: "text",
style: "width: 180px; margin-right: 5px;"
}).appendTo(row);
var fromField = $('<input/>', {
class: "node-input-rule-from",
type: "number",
style: "width: 80px; margin-right: 5px;",
placeholder: "From"
}).appendTo(row);
var toField = $('<input/>', {
class: "node-input-rule-to",
type: "number",
style: "width: 80px;",
placeholder: "To"
}).appendTo(row);
function updateVisibility() {
var t = typeField.val();
if (t === "idx") {
fromField.show();
toField.show();
} else {
fromField.hide();
toField.hide();
}
}
typeField.on("change", updateVisibility);
typeField.val(rule.t || "eq");
valueField.val(rule.v || "");
if (rule.from !== undefined) {
fromField.val(rule.from);
}
if (rule.to !== undefined) {
toField.val(rule.to);
}
updateVisibility();
},
removable: true,
sortable: true
});
(rules || []).forEach(function (r) {
$("#node-input-rules").editableList('addItem', r);
});
},
oneditsave: function () {
var rules = [];
$("#node-input-rules").editableList('items').each(function () {
var row = $(this);
var type = $(".node-input-rule-type", row).val() || "eq";
var value = $(".node-input-rule-value", row).val() || "";
var from = $(".node-input-rule-from", row).val();
var to = $(".node-input-rule-to", row).val();
var rule = { t: type, v: value };
if (type === "idx") {
if (from !== "") {
rule.from = parseInt(from, 10);
}
if (to !== "") {
rule.to = parseInt(to, 10);
}
}
rules.push(rule);
});
this.rules = JSON.stringify(rules);
} }
}); });
</script> </script>
@@ -138,8 +42,25 @@ RED.nodes.registerType('xilica-subscribe', {
</select> </select>
</div> </div>
<div class="form-row"> <div class="form-row">
<label>Rules</label> <label for="node-input-mode">Mode</label>
<ol id="node-input-rules"></ol> <select id="node-input-mode">
<option value="list">Explicit list</option>
<option value="startsWith">Starts with</option>
<option value="endsWith">Ends with</option>
<option value="startsAndEnds">Starts with and ends with</option>
</select>
</div>
<div class="form-row">
<label for="node-input-targets">Targets</label>
<textarea id="node-input-targets" rows="5" style="width: 100%;"></textarea>
</div>
<div class="form-row">
<label for="node-input-startIndex">Start index</label>
<input type="number" id="node-input-startIndex" min="0">
</div>
<div class="form-row">
<label for="node-input-endIndex">End index</label>
<input type="number" id="node-input-endIndex" min="0">
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-transport">Transport</label> <label for="node-input-transport">Transport</label>
@@ -154,10 +75,12 @@ RED.nodes.registerType('xilica-subscribe', {
<p>Builds <code>SUBSCRIBE</code> or <code>UNSUBSCRIBE</code> commands for a Xilica Solaro processor.</p> <p>Builds <code>SUBSCRIBE</code> or <code>UNSUBSCRIBE</code> commands for a Xilica Solaro processor.</p>
<p>On each input message, the node builds the commands and sends them directly to the selected <code>xilica-connection</code>. It then outputs a message with the commands that were sent.</p> <p>On each input message, the node builds the commands and sends them directly to the selected <code>xilica-connection</code>. It then outputs a message with the commands that were sent.</p>
<p><strong>Action</strong> selects whether to generate <code>SUBSCRIBE</code> or <code>UNSUBSCRIBE</code> commands.</p> <p><strong>Action</strong> selects whether to generate <code>SUBSCRIBE</code> or <code>UNSUBSCRIBE</code> commands.</p>
<p><strong>Rules</strong> define which control names to generate, similar to the Switch node's rule list:</p> <p><strong>Mode</strong> controls how the Targets field is interpreted:</p>
<ul> <ul>
<li><strong>Type ==</strong>: the Value field is used as a full control name (e.g. <code>MASTER_GAIN</code>).</li> <li><strong>Explicit list</strong>: each non-empty line in Targets is used as a full control name (e.g. <code>MASTER_GAIN</code>).</li>
<li><strong>Type Indexed</strong>: the Value field is a base string (e.g. <code>CH</code>), and the From/To fields define a numeric range (e.g. 148 <code>CH1</code><code>CH48</code>).</li> <li><strong>Starts with</strong>: each line in Targets is treated as a prefix (e.g. <code>CH</code>), and Start/End index define the numeric range (e.g. 148 <code>CH1</code><code>CH48</code>).</li>
<li><strong>Ends with</strong>: each line in Targets is treated as a suffix (e.g. <code>_OUT</code>), and Start/End index define the numeric range (e.g. 14 <code>1_OUT</code><code>4_OUT</code>).</li>
<li><strong>Starts with and ends with</strong>: each line in Targets is interpreted as <code>prefix|suffix</code> (for example <code>CH|OUT</code>), and Start/End index define the numeric range (e.g. 428 <code>CH4OUT</code><code>CH28OUT</code>).</li>
</ul> </ul>
<p><strong>Transport</strong> selects the subscription transport (<code>TCP</code> or <code>UDP</code>). The node generates commands like <code>SUBSCRIBE CH1 "TCP"</code> or <code>SUBSCRIBE CH1 "UDP"</code> and sends them over the selected connection.</p> <p><strong>Transport</strong> selects the subscription transport (<code>TCP</code> or <code>UDP</code>). The node generates commands like <code>SUBSCRIBE CH1 "TCP"</code> or <code>SUBSCRIBE CH1 "UDP"</code> and sends them over the selected connection.</p>
<p>The Xilica device connection (IP/port/password) is chosen directly on this node via the <strong>Connection</strong> field.</p> <p>The Xilica device connection (IP/port/password) is chosen directly on this node via the <strong>Connection</strong> field.</p>

View File

@@ -1,57 +1,80 @@
const net = require("net"); const net = require("net");
module.exports = function (RED) { module.exports = function (RED) {
function buildControlNamesFromRules(rules) { function buildNamesFromList(targets) {
return targets
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l.length > 0);
}
function buildNamesStartsWith(targets, startIndex, endIndex) {
const prefixes = buildNamesFromList(targets);
const names = []; const names = [];
if (!Array.isArray(rules)) { if (startIndex > endIndex) {
return names; const tmp = startIndex;
startIndex = endIndex;
endIndex = tmp;
} }
prefixes.forEach((prefix) => {
rules.forEach((rule) => { for (let i = startIndex; i <= endIndex; i += 1) {
if (!rule) { names.push(prefix + i);
return;
}
const type = (rule.t || rule.type || "eq").toString();
if (type === "idx") {
const base = (rule.v || rule.base || "").trim();
if (!base) {
return;
}
let from = parseInt(rule.from, 10);
let to = parseInt(rule.to, 10);
if (Number.isNaN(from) && Number.isNaN(to)) {
return;
}
if (Number.isNaN(from)) {
from = to;
}
if (Number.isNaN(to)) {
to = from;
}
if (from > to) {
const tmp = from;
from = to;
to = tmp;
}
for (let i = from; i <= to; i += 1) {
names.push(base + i);
}
} else {
const name = (rule.v || rule.value || "").trim();
if (name) {
names.push(name);
}
} }
}); });
return names; return names;
} }
function buildNamesEndsWith(targets, startIndex, endIndex) {
const suffixes = buildNamesFromList(targets);
const names = [];
if (startIndex > endIndex) {
const tmp = startIndex;
startIndex = endIndex;
endIndex = tmp;
}
suffixes.forEach((suffix) => {
for (let i = startIndex; i <= endIndex; i += 1) {
names.push(String(i) + suffix);
}
});
return names;
}
function buildNamesStartsAndEnds(targets, startIndex, endIndex) {
const patterns = buildNamesFromList(targets);
const names = [];
if (startIndex > endIndex) {
const tmp = startIndex;
startIndex = endIndex;
endIndex = tmp;
}
patterns.forEach((pattern) => {
const parts = pattern.split("|");
const prefix = (parts[0] || "").trim();
const suffix = (parts[1] || "").trim();
if (!prefix && !suffix) {
return;
}
for (let i = startIndex; i <= endIndex; i += 1) {
names.push(prefix + i + suffix);
}
});
return names;
}
function buildControlNames(mode, targets, startIndex, endIndex) {
if (mode === "startsWith") {
return buildNamesStartsWith(targets, startIndex, endIndex);
}
if (mode === "endsWith") {
return buildNamesEndsWith(targets, startIndex, endIndex);
}
if (mode === "startsAndEnds") {
return buildNamesStartsAndEnds(targets, startIndex, endIndex);
}
return buildNamesFromList(targets);
}
function normaliseAction(action) { function normaliseAction(action) {
const a = (action || "").toString().toLowerCase(); const a = (action || "").toString().toLowerCase();
if (a === "unsubscribe" || a === "unsub") { if (a === "unsubscribe" || a === "unsub") {
@@ -65,17 +88,12 @@ module.exports = function (RED) {
const node = this; const node = this;
node.action = config.action || "subscribe"; node.action = config.action || "subscribe";
let storedRules = config.rules;
if (typeof storedRules === "string") {
try {
storedRules = JSON.parse(storedRules);
} catch (e) {
storedRules = [];
}
}
node.rules = Array.isArray(storedRules) ? storedRules : [];
node.transport = config.transport || "TCP";
node.connectionConfig = RED.nodes.getNode(config.connection); node.connectionConfig = RED.nodes.getNode(config.connection);
node.mode = config.mode || "list";
node.targets = config.targets || "";
node.startIndex = parseInt(config.startIndex, 10) || 1;
node.endIndex = parseInt(config.endIndex, 10) || node.startIndex;
node.transport = config.transport || "TCP";
node.on("input", (msg, send, done) => { node.on("input", (msg, send, done) => {
send = send =
@@ -86,16 +104,26 @@ module.exports = function (RED) {
done = done || function () {}; done = done || function () {};
const action = normaliseAction(msg.action || node.action); const action = normaliseAction(msg.action || node.action);
let rules = msg.rules || node.rules; const mode = (msg.mode || node.mode || "list").toString();
if (typeof rules === "string") {
try { const targets =
rules = JSON.parse(rules); typeof msg.targets === "string" && msg.targets.trim().length
} catch (e) { ? msg.targets
rules = []; : node.targets;
let startIndex = node.startIndex;
let endIndex = node.endIndex;
if (Object.prototype.hasOwnProperty.call(msg, "startIndex")) {
const v = parseInt(msg.startIndex, 10);
if (!Number.isNaN(v)) {
startIndex = v;
} }
} }
if (!Array.isArray(rules)) { if (Object.prototype.hasOwnProperty.call(msg, "endIndex")) {
rules = []; const v = parseInt(msg.endIndex, 10);
if (!Number.isNaN(v)) {
endIndex = v;
}
} }
const transport = const transport =
@@ -103,7 +131,7 @@ module.exports = function (RED) {
? msg.transport.trim() ? msg.transport.trim()
: node.transport; : node.transport;
const names = buildControlNamesFromRules(rules); const names = buildControlNames(mode, targets || "", startIndex, endIndex);
if (!names.length) { if (!names.length) {
node.warn("xilica-subscribe: no rules defined"); node.warn("xilica-subscribe: no rules defined");