@janart19/node-red-fusebox
Version:
A collection of Fusebox-specific custom nodes for Node-RED
927 lines (738 loc) • 40 kB
HTML
<script type="text/javascript">
RED.nodes.registerType("fusebox-write-static-data-streams", {
category: 'fusebox',
color: '#f5706c',
defaults: {
name: { value: "" },
controller: { value: "", type: "fusebox-controller", required: true },
outputMode: { value: "", validate: function (v) { return ["all", "change"].includes(v); } },
payloadType: { value: "dynamic", validate: function (v) { return ["static", "dynamic"].includes(v); } },
mappings: { value: [] } // Store multiple mappings
},
inputs: 1,
outputs: 1,
icon: 'font-awesome/fa-send',
label: function () {
const mappings = this.mappings;
let defaultName = `send data streams`;
if (mappings) defaultName = `send data streams: ${mappings.length}`
return this.name || defaultName;
},
paletteLabel: function () {
return "send data streams";
},
oneditprepare: function () {
const node = this;
let _controller = {};
// Load existing mappings or initialize an empty array
const mappings = node.mappings || [];
// Populate form with node values
$('#node-input-name').val(node.name);
$('#node-input-controller').val(node.controller);
$('#node-input-outputMode').val(node.outputMode);
$('#node-input-payloadType').val(node.payloadType);
// Define event listeners for form fields
$("#node-input-controller").change(function () {
queryControllerConfig();
});
$("#mappings-container, #node-input-payloadType, #node-input-outputMode").change(function () {
updateTipText();
});
$("#node-input-payloadType").change(function () {
const rows = getListElements();
for (const elements of rows) {
populatePayload(elements);
validatePayload(elements);
}
});
// Initialize the form fields
initializeEditableList();
// Get the controller node configuration to populate fields and update helper text
function queryControllerConfig() {
const nodeId = $("#node-input-controller").val();
$.getJSON(`fusebox/controllerNodeConfig?id=${nodeId}`, function (data) {
_controller = data;
// Execute functions after successful query
$('#mappings-container').editableList('empty');
$('#mappings-container').editableList('addItems', mappings);
updateTipText();
}).fail(function () {
console.error("Failed to get controller configuration!");
$('#mappings-container').editableList('empty');
$('#mappings-container').editableList('addItems', mappings);
updateTipText();
});
}
// Initialize the EditableList widget
function initializeEditableList() {
$('#mappings-container').editableList({
removable: true,
sortable: true,
header: $("<div>").append($.parseHTML(`
<div style="width: 22px"></div>
<div class="write-static-data-streams-list" style="text-align: center; white-space: normal; flex: 1">
<div class="node-input-topic">Topic</div>
<span class="node-input-arrow"></span>
<div class="checkbox-container">Manual entry</div>
<div class="node-input-keyNameManual">Data stream name</div>
<div class="node-input-index">Member index</div>
<div class="node-input-channelType">Channel type</div>
<div class="node-input-coefficient">Coefficient</div>
<div class="node-input-payload">Payload</div>
</div>
<div style="width: 28px"></div>
`)),
addItem: function (container, index, row) {
addRowElements(container, row);
const elements = getRowElements(container);
formatRow(elements, row);
updateTipText();
attachRowEvents(elements);
},
removeItem: function (data) {
updateTipText();
}
});
}
// Append the row to the container
function addRowElements(container, row) {
const rowHtml = `
<div class="write-static-data-streams-list">
<input type="text" class="node-input-topic" placeholder="Topic" value="${row.topic || ''}">
<span class="node-input-arrow">→</span>
<div class="checkbox-container">
<input type="checkbox" class="node-input-manual" ${row.manual === true ? 'checked' : ''}>
<div class="helper-text">Manual entry</div>
</div>
<select class="node-input-keyNameSelect"></select>
<input type="text" class="node-input-keyNameManual" placeholder="Manual entry, e.g. ABCW" value="${row.keyNameManual || ''}">
<input type="number" class="node-input-index" placeholder="Member" value="${row.index || ''}" min="1" max="16">
<select class="node-input-channelType">
<option value="">Channel type...</option>
<option value="ai" ${row.channelType === 'ai' ? 'selected' : ''}>Analogue input (AI)</option>
<option value="ao" ${row.channelType === 'ao' ? 'selected' : ''}>Analogue output (AO)</option>
<option value="di" ${row.channelType === 'di' ? 'selected' : ''}>Discrete input (DI)</option>
<option value="do" ${row.channelType === 'do' ? 'selected' : ''}>Discrete output (DO)</option>
</select>
<input type="number" class="node-input-coefficient" placeholder="Coef." value="${row.coefficient || ''}" step="0.01">
<input type="number" class="node-input-payload" placeholder="Payload" value="${row.payload || ''}" step="0.01">
</div>`;
container.append(rowHtml);
}
// Get references to the created elements
function getRowElements(container) {
return {
manualElement: container.find('.node-input-manual'),
keySelectElement: container.find('.node-input-keyNameSelect'),
keyManualElement: container.find('.node-input-keyNameManual'),
indexElement: container.find('.node-input-index'),
channelTypeElement: container.find('.node-input-channelType'),
coefficientElement: container.find('.node-input-coefficient'),
payloadElement: container.find('.node-input-payload'),
topicElement: container.find('.node-input-topic')
};
}
// Format the newly created / intialized row's elements
function formatRow(elements, row) {
const services = _controller.services || {};
const keySelectElement = elements.keySelectElement;
const selectedKey = row.keyNameSelect;
// Find the service keys that are writable
const writableKeys = findAllowedWriteSvcKeys(_controller.channels);
const filteredServices = Object.keys(services).reduce((acc, key) => {
if (writableKeys[key]) {
acc[key] = services[key];
}
return acc;
}, {});
const filteredSvcKeys = sortKeysByServiceNames(filteredServices);
const allSvcKeys = sortKeysByServiceNames(services);
clearDropdown(keySelectElement, { text: "Select data stream..." });
// Populate the key selection dropdown with available keys
filteredSvcKeys.forEach(key => {
const serviceName = services[key]?.servicename || "???";
const text = `${serviceName} (${key})`;
const el = $(`<option value="${key}" ${selectedKey === key ? 'selected' : ''}>${text}</option>`);
keySelectElement.append(el);
});
updateKeyName(elements);
validateKeyName(elements);
populateIndex(elements);
validateIndex(elements);
populateChannelType(elements);
validateChannelType(elements);
populatePayload(elements);
validatePayload(elements);
populateCoefficient(elements);
validateCoefficient(elements);
validateTopic(elements);
}
// Attach events to the row elements
function attachRowEvents(elements = {}) {
elements.manualElement.on('change', function () {
updateKeyName(elements);
validateKeyName(elements);
populateIndex(elements);
validateIndex(elements);
populateCoefficient(elements);
validateCoefficient(elements);
});
elements.keySelectElement.on('change', function () {
validateKeyName(elements);
populateIndex(elements);
validateIndex(elements);
populateChannelType(elements);
validateChannelType(elements);
populateCoefficient(elements);
validateCoefficient(elements);
});
elements.keyManualElement.on('change', function () {
populateIndex(elements);
validateIndex(elements);
});
elements.keyManualElement.on('input', function () {
validateKeyName(elements);
populateChannelType(elements);
validateChannelType(elements);
populateCoefficient(elements);
validateCoefficient(elements);
});
elements.channelTypeElement.on('change', function () {
populateIndex(elements);
validateIndex(elements);
populatePayload(elements);
validatePayload(elements);
populateCoefficient(elements);
validateCoefficient(elements);
});
elements.indexElement.on('input', function () {
validateIndex(elements);
});
elements.coefficientElement.on('input', function () {
validateCoefficient(elements);
});
elements.channelTypeElement.on('change', function () {
validateChannelType(elements);
});
elements.topicElement.on('input', function () {
validateTopic(elements);
});
}
// Hide the service key selection dropdown if manual is checked
function updateKeyName(elements) {
const manual = elements.manualElement.is(':checked');
if (manual) {
elements.keySelectElement.val("");
elements.keySelectElement.hide();
elements.keyManualElement.show();
} else {
elements.keyManualElement.val("");
elements.keySelectElement.show();
elements.keyManualElement.hide();
}
}
// Update coefficient field based on the selected key
function populateCoefficient(elements) {
const channelType = elements.channelTypeElement.val() ?? "";
const keyNameSelect = elements.keySelectElement.val();
const keyNameManual = elements.keyManualElement.val();
const services = _controller.services || {};
const keyName = keyNameSelect || keyNameManual;
if (keyName && services[keyName]) {
const convCoef = services[keyName]?.conv_coef || 1;
elements.coefficientElement.val(convCoef);
elements.coefficientElement.prop('disabled', true);
} else {
elements.coefficientElement.val("1");
elements.coefficientElement.prop('disabled', false);
}
if (channelType.startsWith("d")) {
elements.coefficientElement.val("");
elements.coefficientElement.prop('disabled', true);
}
}
// Update the index element based on the selected key
function populateIndex(elements) {
const keyNameSelect = elements.keySelectElement.val();
let el = elements.indexElement;
const prevIndex = el.val();
const id = Math.random().toString(36).substr(2, 9);
const filteredKeys = findAllowedWriteSvcKeys(_controller.channels);
if (keyNameSelect) {
const indexes = filteredKeys[keyNameSelect] || [];
const options = indexes.map(index => {
const desc = findValueBySvcNameAndMember(_controller.channels, keyNameSelect, index, "desc");
return `<option value="${index}" ${prevIndex == index ? "selected" : ""}>${index} ${desc ? `(${desc})` : ``}</option>`
})
el.replaceWith(`<select data-id="${id}" class="node-input-index">${options.join('')}</select>`);
} else {
el.replaceWith(`<input data-id="${id}" type="number" class="node-input-index" placeholder="Member" value="${prevIndex}" min="1" max="16">`);
}
el = elements.indexElement = $(`.node-input-index[data-id="${id}"]`); // Update the reference using the custom attribute
// Reattach the event listener to the new element
el.on('input', function () {
validateIndex(elements);
});
}
// Populate the data stream type dropdown based on the selected key
// Only show the types which are compatible with the selected key and index
function populateChannelType(elements) {
const channels = _controller.channels || {};
const keyNameSelect = elements.keySelectElement.val();
const keyNameManual = elements.keyManualElement.val();
const index = parseInt(elements.indexElement.val());
const el = elements.channelTypeElement;
const prevType = el.val();
let options = [
{ value: "ai", text: "Analogue input (AI)" },
{ value: "ao", text: "Analogue output (AO)" },
{ value: "di", text: "Discrete input (DI)" },
{ value: "do", text: "Discrete output (DO)" }
];
// If the key is selected from the dropdown, find the compatible types
if (!isNaN(index) && keyNameSelect && channels[keyNameSelect] && channels[keyNameSelect][index]) {
const channel = channels[keyNameSelect];
const member = channel[index];
if (member.regtype === "s" || getBitWeights(member.cfg).includes(32768)) {
if (member._type === "analogue") {
options = [{ value: "ai", text: "Analogue input (AI)" }];
} else if (member._type === "discrete") {
options = [{ value: "di", text: "Discrete input (DI)" }];
} else {
options = [
{ value: "ai", text: "Analogue input (AI)" },
{ value: "di", text: "Discrete input (DI)" },
];
}
} else if (["h", "c"].includes(member.regtype) && member._output) {
if (member._type === "analogue") {
options = [{ value: "ao", text: "Analogue output (AO)" }];
} else if (member._type === "discrete") {
options = [{ value: "do", text: "Discrete output (DO)" }];
} else {
options = [
{ value: "ao", text: "Analogue output (AO)" },
{ value: "do", text: "Discrete output (DO)" }
];
}
}
}
const prevTypeValid = options.map(opt => opt.value).includes(prevType);
clearDropdown(el, { text: "Select data stream type...", disabled: true, selected: !prevTypeValid });
options.forEach(option => {
el.append(`<option value="${option.value}" ${(prevType == option.value || options.length === 1) ? "selected" : ""}>${option.text}</option>`);
});
}
// Toggle the payload input based on channel type
// Discrete data streams only accept 0 or 1 as payload
// Analogue data streams accept any float value
function populatePayload(elements) {
const payloadType = $('#node-input-payloadType').val();
const channelType = elements.channelTypeElement.val() ?? "";
let el = elements.payloadElement;
const prevPayload = el.val();
const id = Math.random().toString(36).substr(2, 9);
if (channelType.startsWith("d") && payloadType !== "dynamic") {
el.replaceWith(
`<select data-id="${id}" class="node-input-payload">
<option value="0" ${prevPayload == 0 ? "selected" : ""}>0</option>
<option value="1" ${prevPayload == 1 ? "selected" : ""}>1</option>
</select>`
);
} else {
el.replaceWith(`<input data-id="${id}" type="number" class="node-input-payload" placeholder="Payload" value="${prevPayload}" step="0.01">`);
}
el = elements.payloadElement = $(`.node-input-payload[data-id="${id}"]`); // Update the reference using the custom attribute
// Reattach the event listener to the new element
el.on('input', function () {
validatePayload(elements);
});
if (payloadType === "dynamic") {
el.val("");
el.prop('disabled', true);
}
}
// Clear the data stream selection
function clearDropdown(element, options = {}) {
const { text = "none", disabled = false, selected = false } = options;
element.empty();
element.append($(`<option value="" ${disabled ? 'disabled' : ''} ${selected ? 'selected' : ''}>${text}</option>`));
}
// Update tip text based on current settings
function updateTipText() {
const rows = getListElements();
const payloadType = $('#node-input-payloadType').val();
const topics = rows.map(row => row.topicElement.val());
const payloads = rows.map(row => row.payloadElement.val());
const topic1 = topics && topics.length > 0 ? topics[0] : "topic1";
const topic2 = topics && topics.length > 1 ? topics[1] : "topic2";
const payload1 = payloads && payloads.length > 0 ? payloads[0] : "payload1";
const payload2 = payloads && payloads.length > 1 ? payloads[1] : "payload2";
const tipText1 = `Routing ${rows.length} values from controller (${_controller.uniqueId || "???"})'s data streams.`;
const tipText2 = `The payload will be multiplied by the coefficient (if applicable).`;
const tipText3 = payloadType === "dynamic" ? `The required payload(s) will be searched from the incoming 'msg' object.` : `The payload(s) defined in the form fields above will be used when writing data.`;
const tipText4 = `Only writable data streams and members are shown in the dropdown lists.`;
const tipText5 = payloadType === "dynamic" ? `Incoming message formats:
1. {"topic": topic, "payload": value} to write a single stream
2. {"${topic1}": ${payload1}, "${topic2}": ${payload2}, ...} to write multiple streams`
: `Incoming message formats:
1. {"topic": topic} to write a single stream
2. {"${topic1}": true, "${topic2}": false, ...} to write multiple streams
3. none, in which case every stream will be written.`;
$("#node-input-tip-text-1").text(tipText1);
$("#node-input-tip-text-2").text(tipText2);
$("#node-input-tip-text-3").text(tipText3);
$("#node-input-tip-text-4").text(tipText4);
$("#node-input-tip-text-5").text(tipText5);
}
// Custom validation functions below
function validateKeyName(elements) {
const keyNameSelect = elements.keySelectElement.val();
const keyNameManual = elements.keyManualElement.val();
const invalidValues = ["", undefined, null];
let valid;
if (invalidValues.includes(keyNameSelect) && invalidValues.includes(keyNameManual)) {
valid = false
} else {
valid = true
}
elements.keySelectElement.css('border-color', valid ? '' : 'red');
elements.keyManualElement.css('border-color', valid ? '' : 'red');
}
function validateIndex(elements) {
const index = elements.indexElement.val();
let valid;
valid = isValidIndex(index);
elements.indexElement.css('border-color', valid ? '' : 'red');
}
function validateCoefficient(elements) {
const coefficient = elements.coefficientElement.val();
const channelType = elements.channelTypeElement.val() ?? "";
let valid;
valid = channelType.startsWith("d") || isValidFloat(coefficient);
elements.coefficientElement.css('border-color', valid ? '' : 'red');
}
function validateChannelType(elements) {
const channelType = elements.channelTypeElement.val();
let valid;
valid = isValidChannelType(channelType);
elements.channelTypeElement.css('border-color', valid ? '' : 'red');
}
function validatePayload(elements) {
const payloadType = $('#node-input-payloadType').val();
const channelType = elements.channelTypeElement.val() ?? "";
const payload = elements.payloadElement.val();
let valid;
if (channelType.startsWith("d")) {
valid = ["0", "1"].includes(payload);
} else {
valid = isValidFloat(payload);
}
if (payloadType === "dynamic") {
valid = true;
}
elements.payloadElement.css('border-color', valid ? '' : 'red');
}
function validateTopic(elements) {
const topic = elements.topicElement.val();
const invalidValues = ["", undefined, null];
let duplicateTopics = [];
const rows = getListElements();
for (const elements of rows) {
const t = elements.topicElement.val();
if (t === topic) duplicateTopics.push(t);
}
let valid;
valid = !invalidValues.includes(topic) && duplicateTopics.length <= 1;
elements.topicElement.css('border-color', valid ? '' : 'red');
}
function isValidIndex(index) {
const indexInt = parseInt(index);
return Number.isInteger(indexInt) && indexInt >= 1 && indexInt <= 16;
}
function isValidFloat(value) {
return !isNaN(parseFloat(value));
}
function isValidChannelType(type) {
return ["ai", "ao", "di", "do"].includes(type);
}
// Data stream helper functions below
// Sort alphabetically, all keys starting with "_" are to be in the end
function sortKeysByServiceNames(obj) {
const keyNames = Object.keys(obj);
const normalKeys = keyNames.filter(key => !obj[key]?.servicename.startsWith('_'));
const underscoreKeys = keyNames.filter(key => obj[key]?.servicename.startsWith('_'));
// Sort both arrays in ascending alphabetical order based on servicename
normalKeys.sort((a, b) => obj[a]?.servicename?.localeCompare(obj[b]?.servicename));
underscoreKeys.sort((a, b) => obj[a]?.servicename?.localeCompare(obj[b]?.servicename));
return normalKeys.concat(underscoreKeys);
}
// Only certain services and members are allowed to be written to.
// Retrieve the members and descriptions of the allowed services.
// Requirements:
// 1. all regtype "s" services
// 2. all chantype "mb" services with (regtype "h" or "c") and (mbi,mba,regadd existing in output table or bit includes 32768)
function findAllowedWriteSvcKeys(channels = {}) {
const result = {};
// 1
for (const key in channels) {
const channel = channels[key];
for (index in channel) {
const member = channel[index];
const indexInt = parseInt(index);
if (member.regtype === "s") {
result[key] = result[key] || [];
if (!result[key].includes(indexInt)) {
result[key].push(indexInt);
}
}
}
}
// 2
for (const key in channels) {
const channel = channels[key];
for (index in channel) {
const member = channel[index];
const indexInt = parseInt(index);
if ((member.chantype === "mb" && (["h", "c"].includes(member.regtype)) && (member._output || getBitWeights(member.cfg).includes(32768)))) {
result[key] = result[key] || [];
if (!result[key].includes(indexInt)) {
result[key].push(indexInt);
}
}
}
}
return result;
}
function findValueBySvcNameAndMember(channels = {}, name, member, key) {
return channels?.[name]?.[member]?.[key] ?? null;
}
function findIndexesBySvcName(channels = {}, name) {
return Object.keys(channels?.[name]) ?? [];
}
function getBitWeights(decimalNumber) {
let bitWeights = [];
let bitValue = 1;
while (decimalNumber > 0) {
if (decimalNumber & 1) {
bitWeights.push(bitValue);
}
decimalNumber >>= 1;
bitValue <<= 1;
}
return bitWeights;
};
// Iterate over each mapping row and store the elements
function getListElements() {
const result = [];
$('#mappings-container').editableList('items').each(function () {
const container = $(this);
const elements = getRowElements(container);
result.push(elements);
});
return result;
}
},
oneditsave: function () {
const node = this;
node.name = $('#node-input-name').val();
node.controller = $("#node-input-controller").val();
node.outputMode = $('#node-input-outputMode').val();
node.payloadType = $('#node-input-payloadType').val();
node.mappings = getListValues();
// Iterate over each mapping row and store the values
function getListValues() {
const mappings = [];
$('#mappings-container').editableList('items').each(function () {
const container = $(this);
const manual = container.find('.node-input-manual').is(':checked');
const keyNameSelect = container.find('.node-input-keyNameSelect').val();
const keyNameManual = container.find('.node-input-keyNameManual').val();
const channelType = container.find('.node-input-channelType').val();
const index = parseInt(container.find('.node-input-index').val());
const coefficient = parseFloat(container.find('.node-input-coefficient').val());
const payload = parseFloat(container.find('.node-input-payload').val());
const topic = container.find('.node-input-topic').val();
mappings.push({ manual, keyNameSelect, keyNameManual, channelType, index, coefficient, payload, topic });
});
return mappings;
}
}
});
</script>
<!-- Define style for the form fields -->
<style type="text/css">
.write-static-data-streams-div .form-row {
margin-bottom: 10px;
}
.red-ui-editableList-header {
background-color: #80808014;
font-weight: bold;
display: flex;
}
.red-ui-editableList-container {
min-height: 50px;
}
.write-static-data-streams-div .form-row label {
width: 33% ;
vertical-align: middle;
}
.write-static-data-streams-div .form-row div,
.write-static-data-streams-div .form-row input,
.write-static-data-streams-div .form-row select {
max-width: 66% ;
}
.write-static-data-streams-div .form-row select {
width: 66% ;
}
.write-static-data-streams-div .form-tips {
max-width: 100% ;
text-align: center;
}
/* Editable list style */
.write-static-data-streams-div .red-ui-editableList {
margin-bottom: 10px;
min-width: 700px;
}
.write-static-data-streams-list {
overflow: hidden;
white-space: nowrap;
display: flex;
align-items: center;
}
.write-static-data-streams-list .checkbox-container {
text-align: center;
min-width: 35px;
width: 7%;
}
.write-static-data-streams-list .helper-text {
font-size: 10px;
line-height: 12px;
text-wrap: wrap;
color: #666;
}
.write-static-data-streams-list .node-input-manual {
min-width: 16px;
min-height: 16px;
}
.write-static-data-streams-list .node-input-keyNameManual {
font-size: 12px ;
min-width: 150px;
width: 30%;
}
.write-static-data-streams-list .node-input-keyNameSelect {
font-size: 12px;
min-width: 150px;
width: 30%;
}
.write-static-data-streams-list .node-input-channelType {
font-size: 12px;
min-width: 100px;
width: 15%;
}
.write-static-data-streams-list .node-input-index {
font-size: 12px ;
min-width: 85px;
width: 15%;
}
.write-static-data-streams-list .node-input-coefficient {
font-size: 12px ;
min-width: 65px;
width: 7%;
}
.write-static-data-streams-list .node-input-payload {
font-size: 12px ;
min-width: 75px;
width: 7%;
}
.write-static-data-streams-list .node-input-arrow {
font-size: 18px;
margin-left: 4px;
margin-right: 4px;
min-width: 20px;
width: 4%;
}
.write-static-data-streams-list .node-input-topic {
font-size: 12px ;
min-width: 100px;
width: 15%;
}
</style>
<!-- Form fields for the Data Stream Router node -->
<script type="text/html" data-template-name="fusebox-write-static-data-streams">
<div class="write-static-data-streams-div">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-controller"><i class="fa fa-search"></i> Controller</label>
<select id="node-input-controller">
<option value="" disabled selected>Select controller...</option>
</select>
</div>
<div class="form-row">
<label for="node-input-outputMode"><i class="fa fa-sign-out"></i> Write mode</label>
<select id="node-input-outputMode">
<option value="" disabled selected>Select write mode...</option>
<option value="all">Write data every time</option>
<option value="change">Write data on change</option>
</select>
</div>
<div class="form-row">
<label for="node-input-payloadType"><i class="fa fa-sign-out"></i> Payload type</label>
<select id="node-input-payloadType">
<option value="" disabled>Select payload type...</option>
<option value="static">Use form values</option>
<option value="dynamic" selected>Use incoming 'msg' payload</option>
</select>
</div>
<ol id="mappings-container"></ol>
<div class="form-tips" id="node-input-tip-text-1"></div>
<div class="form-tips" id="node-input-tip-text-2"></div>
<div class="form-tips" id="node-input-tip-text-3"></div>
<div class="form-tips" id="node-input-tip-text-4"></div>
<div class="form-tips" id="node-input-tip-text-5" style="white-space: pre-line;"></div>
<br>
</div>
</script>
<!-- Define node description -->
<script type="text/html" data-help-name="fusebox-write-static-data-streams">
<p>
Write (save) the formatted values of one or more data streams back to the devices.
Widely customizable node that can format data in various formats.
</p>
<p>Often used at the end of some flow to route a value back to the data stream.</p>
<h3>Parameters</h3>
<dl class="message-properties">
<dt class="optional">name <span class="property-type">string</span></dt>
<dd>User-friendly name for the node</dd>
<dt>controller <span class="property-type">controller</span></dt>
<dd>The source of the data streams on the local network. Localhost (127.0.0.1) for most cases.</dd>
<dt>write mode <span class="property-type">string</span></dt>
<dd>Specifies the conditions when data will be saved. Selecting <code>change</code> will write data only when the data stream differs from the previous value.</dd>
<dt>payload type <span class="property-type">string</span></dt>
<dd>Specifies the source of payload values, either from the mapping fields or from the incoming <code>msg</code> object.</dd>
<dt>mappings <span class="property-type">object</span></dt>
<dd>Each row specifies a data stream member, channel type (analogue or discrete), and a topic to use when writing the message. Manual data stream entries are also allowed.</dd>
</dl>
<h3>Output</h3>
<dl class="message-properties">
<dt>controller <span class="property-type">object</span></dt>
<dd>Each output <code>msg</code> includes info about the source of the data, e.g. <code>uniqueId</code>, <code>host</code>, <code>protocol</code>.</dd>
<dt>result <span class="property-type">boolean</span></dt>
<dd><code>true</code> if the data was successfully written to the device, <code>false</code> otherwise.</dd>
<dt>parameters <span class="property-type">object</span></dt>
<dd>Contains info about the data stream which was written to the device, e.g. <code>data stream name</code>, <code>channel type</code>, <code>final payload</code>.</dd>
</dl>
<h3>Additional details</h3>
<p>This node gets its data from the controller configuration and the global context.</p>
<p>In order to preserve the unit of the defined data stream, the payload is multiplied by the coefficient (if applicable).</p>
<p>The exact payload format depends on the message type. Example incoming messages:</p>
<p>
In case of <code>separate</code> message types: <code style="white-space: normal;">{"topic": string, "payload": float}</code>.
<br>
In case of <code>comprehensive</code> message type: <code style="white-space: normal;">{"topic1": float, "topic2": float, ...}</code>.
</p>
<p>Any other incoming <code>msg</code> properties will be preserved.</p>
</script>