first contrib
This commit is contained in:
@@ -2,16 +2,138 @@
|
||||
RED.nodes.registerType('xilica-ping', {
|
||||
category: 'Xilica',
|
||||
color: '#D8E7FF',
|
||||
defaults: { name: {value:""} },
|
||||
defaults: {
|
||||
name: { value: "" }
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
icon: "font-awesome/fa-plug",
|
||||
label: function () { return this.name || "xilica ping"; }
|
||||
});
|
||||
|
||||
RED.nodes.registerType('xilica-connection', {
|
||||
category: 'config',
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
host: { value: "", required: true },
|
||||
port: { value: 10007, required: true },
|
||||
timeout: { value: 5000 }
|
||||
},
|
||||
credentials: {
|
||||
password: { type: "password" }
|
||||
},
|
||||
label: function () {
|
||||
if (this.name) {
|
||||
return this.name;
|
||||
}
|
||||
if (this.host && this.port) {
|
||||
return this.host + ":" + this.port;
|
||||
}
|
||||
return "Xilica connection";
|
||||
}
|
||||
});
|
||||
|
||||
RED.nodes.registerType('xilica-command', {
|
||||
category: 'Xilica',
|
||||
color: '#D8E7FF',
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
connection: { value: "", type: "xilica-connection", required: true },
|
||||
command: { value: "" },
|
||||
control: { value: "" },
|
||||
data: { value: "" },
|
||||
parseResponse: { value: true }
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
icon: "font-awesome/fa-plug",
|
||||
label: function () {
|
||||
if (this.name) {
|
||||
return this.name;
|
||||
}
|
||||
if (this.command) {
|
||||
return "xilica " + this.command.toLowerCase();
|
||||
}
|
||||
return "xilica command";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="xilica-ping">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name">Name</label>
|
||||
<input type="text" id="node-input-name">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="xilica-ping">
|
||||
<p>Simple test node that returns a fixed payload to confirm the Xilica nodes module is installed.</p>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="xilica-connection">
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-name">Name</label>
|
||||
<input type="text" id="node-config-input-name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-host">Host</label>
|
||||
<input type="text" id="node-config-input-host" placeholder="192.168.x.x">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-port">TCP Port</label>
|
||||
<input type="number" id="node-config-input-port" min="1" max="65535">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-timeout">Timeout (ms)</label>
|
||||
<input type="number" id="node-config-input-timeout" min="100" max="60000">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-password">Password</label>
|
||||
<input type="password" id="node-config-input-password">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="xilica-connection">
|
||||
<p>Configuration for connecting to a Xilica Solaro processor using the Third-Party Control API.</p>
|
||||
<p>Specify the device IP address and TCP port (default 10007). Optionally set a password if the device is locked.</p>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="xilica-command">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name">Name</label>
|
||||
<input type="text" id="node-input-name">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-connection">Connection</label>
|
||||
<input type="text" id="node-input-connection">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-command">Command</label>
|
||||
<input type="text" id="node-input-command" placeholder="SET, GET, GETRAW, INC, ...">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-control">Control Object/Group</label>
|
||||
<input type="text" id="node-input-control" placeholder="gain1, $group1, ...">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-data">Data</label>
|
||||
<input type="text" id="node-input-data" placeholder="-3.2, "Butterworth", TRUE, ...">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-parseResponse">Parse response</label>
|
||||
<input type="checkbox" id="node-input-parseResponse" style="width:auto;">
|
||||
<span>Return structured payload instead of raw text</span>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="xilica-command">
|
||||
<p>Send a single command to a Xilica Solaro processor using the Third-Party Control API over TCP.</p>
|
||||
<p>You can either:</p>
|
||||
<ul>
|
||||
<li>Configure the command, control object and data fields in the node, or</li>
|
||||
<li>Provide a complete command string in <code>msg.payload</code>, or</li>
|
||||
<li>Provide <code>msg.command</code>, <code>msg.control</code> and <code>msg.data</code>.</li>
|
||||
</ul>
|
||||
<p>The node automatically appends the required carriage return and waits for the response. When parsing is enabled, it returns an object (or array of objects) describing values, notifications, OK and error responses. When disabled, it returns the raw response lines as a single string.</p>
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,12 +1,267 @@
|
||||
const net = require("net");
|
||||
|
||||
module.exports = function (RED) {
|
||||
function XilicaPing(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
this.on("input", (msg, send, done) => {
|
||||
send = send || function () { this.send(msg); }.bind(this);
|
||||
msg.payload = "xilica-ping node is alive";
|
||||
send(msg);
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RED.nodes.registerType("xilica-ping", XilicaPing);
|
||||
|
||||
function XilicaConnection(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
this.host = config.host;
|
||||
this.port = parseInt(config.port, 10) || 10007;
|
||||
this.timeout = parseInt(config.timeout, 10) || 5000;
|
||||
this.credentials = this.credentials || {};
|
||||
}
|
||||
|
||||
RED.nodes.registerType("xilica-connection", XilicaConnection, {
|
||||
credentials: {
|
||||
password: { type: "password" },
|
||||
},
|
||||
});
|
||||
|
||||
function parseResponseLines(lines) {
|
||||
const results = [];
|
||||
lines.forEach((line) => {
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
if (line === "OK") {
|
||||
results.push({ type: "ok" });
|
||||
return;
|
||||
}
|
||||
if (line.startsWith("ERROR=")) {
|
||||
const code = parseInt(line.substring(6), 10);
|
||||
results.push({ type: "error", code, raw: line });
|
||||
return;
|
||||
}
|
||||
if (line[0] === "#") {
|
||||
const payload = line.slice(1);
|
||||
const idx = payload.indexOf("=");
|
||||
if (idx !== -1) {
|
||||
const control = payload.slice(0, idx);
|
||||
const data = payload.slice(idx + 1);
|
||||
results.push({
|
||||
type: "notification",
|
||||
control,
|
||||
data,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
const idx = line.indexOf("=");
|
||||
if (idx !== -1) {
|
||||
const control = line.slice(0, idx);
|
||||
const data = line.slice(idx + 1);
|
||||
results.push({
|
||||
type: "value",
|
||||
control,
|
||||
data,
|
||||
});
|
||||
return;
|
||||
}
|
||||
results.push({ type: "raw", raw: line });
|
||||
});
|
||||
|
||||
if (results.length === 1) {
|
||||
return results[0];
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function XilicaCommand(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
const node = this;
|
||||
|
||||
node.connectionConfig = RED.nodes.getNode(config.connection);
|
||||
node.command = config.command || "";
|
||||
node.control = config.control || "";
|
||||
node.data = config.data || "";
|
||||
node.parseResponse = config.parseResponse !== false;
|
||||
|
||||
if (!node.connectionConfig) {
|
||||
node.status({ fill: "red", shape: "ring", text: "no connection" });
|
||||
}
|
||||
|
||||
node.on("input", (msg, send, done) => {
|
||||
send = send || function () { node.send.apply(node, arguments); };
|
||||
done = done || function () {};
|
||||
|
||||
const conn = node.connectionConfig;
|
||||
if (!conn || !conn.host) {
|
||||
node.status({ fill: "red", shape: "ring", text: "no host" });
|
||||
node.error("No Xilica connection configured", msg);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
let rawCommand = "";
|
||||
if (typeof msg.payload === "string" && msg.payload.trim()) {
|
||||
rawCommand = msg.payload.trim();
|
||||
}
|
||||
|
||||
let cmd = node.command;
|
||||
let control = node.control;
|
||||
let data = node.data;
|
||||
|
||||
if (msg.command) {
|
||||
cmd = msg.command;
|
||||
}
|
||||
if (msg.control) {
|
||||
control = msg.control;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(msg, "data")) {
|
||||
data = msg.data;
|
||||
}
|
||||
|
||||
if (!rawCommand) {
|
||||
if (!cmd) {
|
||||
node.status({ fill: "red", shape: "ring", text: "missing command" });
|
||||
node.error("Missing command", msg);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
rawCommand = String(cmd).trim();
|
||||
if (control) {
|
||||
rawCommand += " " + String(control).trim();
|
||||
}
|
||||
if (data !== undefined && data !== "") {
|
||||
rawCommand += " " + String(data).trim();
|
||||
}
|
||||
}
|
||||
|
||||
const host = conn.host;
|
||||
const port = conn.port || 10007;
|
||||
const timeout = conn.timeout || 5000;
|
||||
const password =
|
||||
conn.credentials && conn.credentials.password
|
||||
? conn.credentials.password
|
||||
: null;
|
||||
|
||||
const client = net.createConnection({ host, port });
|
||||
let buffer = "";
|
||||
let phase = password ? "login" : "command";
|
||||
const responseLines = [];
|
||||
let finished = false;
|
||||
let idleTimer = null;
|
||||
const idleTimeout = 100;
|
||||
|
||||
function clearIdle() {
|
||||
if (idleTimer) {
|
||||
clearTimeout(idleTimer);
|
||||
idleTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleIdleFinish() {
|
||||
clearIdle();
|
||||
idleTimer = setTimeout(() => {
|
||||
finishSuccess();
|
||||
}, idleTimeout);
|
||||
}
|
||||
|
||||
function finishSuccess() {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
clearIdle();
|
||||
client.end();
|
||||
node.status({});
|
||||
|
||||
msg.raw = responseLines.slice();
|
||||
if (node.parseResponse) {
|
||||
msg.payload = parseResponseLines(responseLines);
|
||||
} else {
|
||||
msg.payload = responseLines.join("\n");
|
||||
}
|
||||
|
||||
send(msg);
|
||||
done();
|
||||
}
|
||||
|
||||
function finishError(err) {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
clearIdle();
|
||||
client.destroy();
|
||||
node.status({ fill: "red", shape: "ring", text: "error" });
|
||||
node.error(err, msg);
|
||||
done(err);
|
||||
}
|
||||
|
||||
client.setTimeout(timeout);
|
||||
|
||||
client.on("connect", () => {
|
||||
node.status({ fill: "green", shape: "dot", text: "connected" });
|
||||
try {
|
||||
if (phase === "login") {
|
||||
client.write("LOGIN " + password + "\r");
|
||||
} else {
|
||||
client.write(rawCommand + "\r");
|
||||
}
|
||||
} catch (err) {
|
||||
finishError(err);
|
||||
}
|
||||
});
|
||||
|
||||
client.on("data", (dataBuf) => {
|
||||
buffer += dataBuf.toString("utf8");
|
||||
let idx = buffer.indexOf("\r");
|
||||
while (idx !== -1) {
|
||||
const line = buffer.slice(0, idx);
|
||||
buffer = buffer.slice(idx + 1);
|
||||
|
||||
if (phase === "login") {
|
||||
if (line && line.startsWith("ERROR=")) {
|
||||
finishError(new Error("Login failed: " + line));
|
||||
return;
|
||||
}
|
||||
phase = "command";
|
||||
try {
|
||||
client.write(rawCommand + "\r");
|
||||
} catch (err) {
|
||||
finishError(err);
|
||||
}
|
||||
} else {
|
||||
if (line) {
|
||||
responseLines.push(line);
|
||||
}
|
||||
scheduleIdleFinish();
|
||||
}
|
||||
|
||||
idx = buffer.indexOf("\r");
|
||||
}
|
||||
});
|
||||
|
||||
client.on("timeout", () => {
|
||||
finishError(new Error("Timeout waiting for response from Xilica"));
|
||||
});
|
||||
|
||||
client.on("error", (err) => {
|
||||
finishError(err);
|
||||
});
|
||||
|
||||
client.on("end", () => {
|
||||
if (!finished && responseLines.length > 0) {
|
||||
finishSuccess();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
RED.nodes.registerType("xilica-command", XilicaCommand);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user