smithtek-nodered
Version:
NodeRED node to post data on SmithTek using MQTT.
178 lines (141 loc) • 5.14 kB
JavaScript
module.exports = function (RED) {
"use strict";
function StepTimerNode(config) {
RED.nodes.createNode(this, config);
const node = this;
// ---- helpers ----
function toNumber(v, fallback = 0) {
const n = Number(v);
return Number.isFinite(n) ? n : fallback;
}
function clampInt(v, min, max, fallback) {
let n = parseInt(v, 10);
if (!Number.isFinite(n)) n = fallback;
if (n < min) n = min;
if (n > max) n = max;
return n;
}
function unitsToMs(value, units, defaultUnits = "seconds") {
const v = toNumber(value, 0);
const u = units || defaultUnits;
switch (u) {
case "milliseconds": return v;
case "minutes": return v * 60 * 1000;
case "hours": return v * 60 * 60 * 1000;
case "days": return v * 24 * 60 * 60 * 1000;
case "seconds":
default: return v * 1000;
}
}
function safeClearTimeout(t) {
if (t) clearTimeout(t);
return null;
}
// ---- config ----
node.numOutputs = clampInt(config.outputs, 1, 128, 1); // prevent 0 outputs / silly values
node.timeoutVal = Math.max(0, unitsToMs(config.step, config.stepUnits, "seconds"));
node.pauseVal = Math.max(0, unitsToMs(config.pauseTime, config.pauseUnits, "seconds"));
node.autostart = !!config.autostart;
node.startupDelay = node.autostart
? Math.max(0, unitsToMs(config.startupDelay, config.startupUnits, "seconds"))
: 0;
// ---- state ----
node.nextOutput = 0;
node.running = false;
node.paused = false;
node.started = false;
let tickTimer = null; // sequencing loop
let pauseTimer = null; // pause delay
function setStatus(fill, shape, text) {
node.status({ fill, shape, text });
}
function buildNextOutputArray() {
const outs = new Array(node.numOutputs).fill(null);
// fresh message each time (avoid shared reference surprises)
const msg = { payload: true };
outs[node.nextOutput] = msg;
node.nextOutput = (node.nextOutput + 1) % node.numOutputs;
return outs;
}
function stopSequencing() {
tickTimer = safeClearTimeout(tickTimer);
}
function scheduleNextTick() {
// If we’re stopped or paused, don’t schedule.
if (!node.running || node.paused) return;
tickTimer = safeClearTimeout(tickTimer);
// Use setTimeout loop instead of setInterval (clean stop, less drift pain)
tickTimer = setTimeout(() => {
if (!node.running || node.paused) return;
node.send(buildNextOutputArray());
setStatus("blue", "dot", "Running, next output: " + String(node.nextOutput));
scheduleNextTick();
}, node.timeoutVal);
}
function startNow() {
node.running = true;
node.paused = false;
// fire immediately, then schedule next
node.send(buildNextOutputArray());
setStatus("blue", "dot", "Running, next output: " + String(node.nextOutput));
scheduleNextTick();
}
function pauseNow() {
node.running = false;
node.paused = true;
stopSequencing();
pauseTimer = safeClearTimeout(pauseTimer);
setStatus("red", "dot", "Paused, next output: " + String(node.nextOutput));
pauseTimer = setTimeout(() => {
// resume after pause
node.paused = false;
startNow();
}, node.pauseVal);
}
// ---- initial status ----
setStatus("red", "ring", "Not Started");
// ---- input behavior ----
// If not running and not paused: start
// If running and not paused: pause
// If paused: ignore inputs (matches your original intent)
node.on("input", function (msg) {
// Optional: allow msg.payload commands without breaking old behaviour.
// If you don’t want commands, you can delete this whole block.
const p = msg && msg.payload;
const cmd = (typeof p === "string") ? p.toLowerCase().trim() : null;
if (cmd === "stop") {
node.running = false;
node.paused = false;
stopSequencing();
pauseTimer = safeClearTimeout(pauseTimer);
setStatus("red", "ring", "Stopped");
return;
}
if (cmd === "reset") {
node.nextOutput = 0;
setStatus(node.running ? "blue" : "red", "dot", "Reset, next output: 0");
return;
}
if (!node.running && !node.paused) {
startNow();
} else if (!node.paused) {
pauseNow();
}
});
// ---- cleanup on redeploy ----
node.on("close", function () {
stopSequencing();
pauseTimer = safeClearTimeout(pauseTimer);
});
// ---- autostart ----
if (node.autostart && !node.started) {
node.started = true;
setStatus("yellow", "ring", "Autostart in " + Math.round(node.startupDelay / 1000) + "s");
setTimeout(() => {
// Node-RED supported way to inject into self
node.receive({ payload: "start" });
}, node.startupDelay);
}
}
RED.nodes.registerType("smithtek_sequencer", StepTimerNode);
};