UNPKG

@oriolrius/node-red-contrib-s7

Version:

A Node-RED node to interact with Siemens S7 PLCs

1,189 lines (1,040 loc) 72 kB
<script type="text/x-red" data-template-name="s7 endpoint"> <div class="form-row"> <ul style="background: #fff; min-width: 600px; margin-bottom: 20px;" id="node-config-s7-endpoint-tabs"></ul> </div> <div id="node-config-s7-endpoint-tabs-content" style="min-height: 170px;"> <div id="s7-endpoint-tab-connection" style="display:none"> <div class="form-row"> <label for="node-config-input-transport"><i class="fa fa-plug"></i> <span data-i18n="s7.endpoint.label.transport"></span></label> <select type="text" id="node-config-input-transport"> <option value="iso-on-tcp" data-i18n="s7.endpoint.transport.iso-on-tcp"></option> <option value="mpi-s7" data-i18n="s7.endpoint.transport.mpi-s7"></option> </select> </div> <div id="s7-endpoint-transport-iso-on-tcp" style="display:none; padding-left: 20px;"> <div class="form-row"> <label for="node-config-input-address"><i class="fa fa-globe"></i> <span data-i18n="s7.endpoint.label.address"></span></label> <input class="input-append-left" type="text" id="node-config-input-address" data-i18n="[placeholder]s7.endpoint.placeholder.address" style="width: 40%;"> <label for="node-config-input-port" style="margin-left: 10px; width: auto; "> <span data-i18n="s7.endpoint.label.port"></span></label> <input type="text" id="node-config-input-port" data-i18n="[placeholder]s7.endpoint.label.port" style="width: 50px; margin-right: 10px;"> <a id="node-config-lookup-plc-pn" class="btn red-ui-button disabled" data-i18n="[title]s7.endpoint.label.search"><i class="fa fa-search"></i></a> <a id="node-config-flash-plc-pn" class="btn red-ui-button disabled" data-i18n="[title]s7.endpoint.label.flash-led"><i class="fa fa-lightbulb-o"></i></a> </div> <div class="form-row"> <label for="node-config-input-connmode"><i class="fa fa-sliders"></i> <span data-i18n="s7.endpoint.label.connmode"></span></label> <select type="text" id="node-config-input-connmode"> <option value="rack-slot" data-i18n="s7.endpoint.connmode.rack-slot"></option> <option value="tsap" data-i18n="s7.endpoint.connmode.tsap"></option> </select> </div> <div class="form-row" id="node-config-s7-endpoint-mode-rackslot"> <label for="node-config-input-rack"><i class="fa fa-sitemap"></i> <span data-i18n="s7.endpoint.label.rack"></span></label> <input class="input-append-left" type="text" id="node-config-input-rack" data-i18n="[placeholder]s7.endpoint.label.rack" style="width: 50px"> <label for="node-config-input-slot" style="margin-left: 10px; width: 35px; "> <span data-i18n="s7.endpoint.label.slot"></span></label> <input type="text" id="node-config-input-slot" data-i18n="[placeholder]s7.endpoint.label.slot" style="width:50px"> </div> <div class="form-row" id="node-config-s7-endpoint-mode-tsap"> <label for="node-config-input-localtsaphi"><i class="fa fa-exchange"></i> <span data-i18n="s7.endpoint.label.tsap-local"></span></label> <input class="input-append-left" type="text" id="node-config-input-localtsaphi" style="width: 35px"><span><b>.</b></span> <input class="input-append-left" type="text" id="node-config-input-localtsaplo" style="width: 35px"> <label for="node-config-input-remotetsaphi" style="margin-left: 10px"> <span data-i18n="s7.endpoint.label.tsap-remote"></span></label> <input class="input-append-left" type="text" id="node-config-input-remotetsaphi" style="width: 35px"><span><b>.</b></span> <input class="input-append-left" type="text" id="node-config-input-remotetsaplo" style="width: 35px"> </div> </div> <div id="s7-endpoint-transport-mpi-s7" style="display:none; padding-left: 20px;"> <div class="form-row"> <label for="node-config-input-adapter"><i class="fa fa-exchange"></i> <span data-i18n="s7.endpoint.label.adapter"></span></label> <input type="text" id="node-config-input-adapter" data-i18n="[placeholder]s7.endpoint.label.adapter"> <input type="text" id="s7-endpoint-adapter-placeholder" data-i18n="[placeholder]s7.endpoint.label.adapterplaceholder" disabled style="display:none"> </div> <div class="form-row"> <label for="node-config-input-busaddr"><i class="fa fa-sitemap"></i> <span data-i18n="s7.endpoint.label.busaddr"></span></label> <input type="text" id="node-config-input-busaddr" style="width: 60px;"> </div> </div> <div class="form-row"> <label for="node-config-input-cycletime"><i class="fa fa-refresh"></i> <span data-i18n="s7.endpoint.label.cycletime"></span></label> <input type="text" id="node-config-input-cycletime" data-i18n="[placeholder]s7.endpoint.label.cycletime" style="width: 60px;"> <span>ms</span> </div> <div class="form-row"> <label for="node-config-input-timeout"><i class="fa fa-clock-o"></i> <span data-i18n="s7.endpoint.label.timeout"></span></label> <input type="text" id="node-config-input-timeout" data-i18n="[placeholder]s7.endpoint.label.timeout" style="width: 60px;"> <span>ms</span> </div> <div class="form-row"> <label for="node-config-input-rewritetimes"><span data-i18n="s7.endpoint.label.rewritetimes"></span></label> <input type="text" id="node-config-input-rewritetimes" style="width: 60px;"> </div> <div class="form-row"> <label for="node-config-input-rewriteinterval"><span data-i18n="s7.endpoint.label.rewriteinterval"></span></label> <input type="text" id="node-config-input-rewriteinterval" style="width: 60px;"> <span>ms</span> </div> <div class="form-row"> <label for="node-config-input-name"><i class="fa fa-tag"></i> <span data-i18n="s7.label.name"></span></label> <input type="text" id="node-config-input-name" data-i18n="[placeholder]s7.label.name"> </div> </div> <div id="s7-endpoint-tab-variables" style="display:none"> <div class="form-row" style="margin-bottom:0;"> <label><i class="fa fa-list"></i> <span data-i18n="s7.endpoint.label.variables.list"></span></label> </div> <div class="form-row node-input-variables-container-row" style="margin-bottom: 0px;"> <div id="node-config-input-variables-container-div" style="box-sizing: border-box; border-radius: 5px; height: 300px; padding: 5px; border: 1px solid #ccc; overflow-y:scroll;"> <ol id="node-config-input-variables-container" style=" list-style-type:none; margin: 0;"></ol> </div> </div> <div class="form-row"> <a href="#" class="editor-button editor-button-small" id="node-config-s7-endpoint-var-export" style="margin: 4px; float: right"><i class="fa fa-download"></i> <span data-i18n="s7.endpoint.label.variables.export"></span></a> <input type="file" id="node-config-s7-endpoint-var-import" style="display: none"/> <a href="#" class="editor-button editor-button-small" id="node-config-s7-endpoint-var-import-btn" style="margin: 4px; float: right"><i class="fa fa-upload"></i> <span data-i18n="s7.endpoint.label.variables.import"></span></a> <a href="#" class="editor-button editor-button-small" id="node-config-input-add-variable" style="margin: 4px;"><i class="fa fa-plus"></i> <span data-i18n="s7.endpoint.label.variables.add"></span></a> <a href="#" class="editor-button editor-button-small" id="node-config-s7-endpoint-var-clean" style="margin: 4px;"><i class="fa fa-trash-o"></i> <span data-i18n="s7.endpoint.label.variables.clean"></span></a> </div> </div> </div> </script> <script type="text/x-red" data-help-name="s7 endpoint"> <p>🏭 <strong>Configures the connection to a Siemens S7 PLC</strong> (S7-200, S7-300, S7-400, S7-1200, S7-1500) for reading and writing data over Ethernet or serial connections.</p> <h3>📋 Overview</h3> <p>The S7 Endpoint is a <strong>configuration node</strong> that establishes and manages the connection to your PLC. All S7 In, S7 Out, and S7 Control nodes reference this endpoint to communicate with the same PLC instance.</p> <h3>🔌 Connection Configuration</h3> <h4>Transport Method</h4> <dl class="message-properties"> <dt>Ethernet (ISO-on-TCP)<span class="property-type">recommended</span></dt> <dd>Standard TCP/IP connection over Ethernet - most common and reliable method</dd> <dt>MPI/PPI/DP Adapter<span class="property-type">serial</span></dt> <dd>Serial connection via MPI/PPI adapters (requires additional hardware and drivers)</dd> </dl> <h4>🌐 Ethernet Settings</h4> <dl class="message-properties"> <dt>IP Address<span class="property-type">string</span></dt> <dd>The IP address of your PLC (e.g., <code>192.168.1.10</code>). Must be reachable from Node-RED.</dd> <dt>Port<span class="property-type">number</span></dt> <dd>TCP port for S7 communication (default: <code>102</code> - standard S7 port)</dd> <dt>Rack<span class="property-type">number</span></dt> <dd>PLC rack number (usually <code>0</code> for most configurations)</dd> <dt>Slot<span class="property-type">number</span></dt> <dd>CPU slot number (typically <code>2</code> for most PLCs, check your hardware configuration)</dd> </dl> <h4>⚙️ Advanced Settings</h4> <dl class="message-properties"> <dt>Cycle Time<span class="property-type">milliseconds</span></dt> <dd>Interval for automatic reading (<code>1000ms</code> recommended). Set to <code>0</code> to disable automatic reading and use manual triggers only.</dd> <dt>Timeout<span class="property-type">milliseconds</span></dt> <dd>Maximum time to wait for PLC response (<code>8000ms</code> recommended for networks with high latency)</dd> <dt>Rewrite Times<span class="property-type">number</span></dt> <dd>Number of retry attempts for failed write operations (default: <code>3</code>)</dd> <dt>Rewrite Interval<span class="property-type">milliseconds</span></dt> <dd>Delay between write retry attempts (default: <code>100ms</code>)</dd> </dl> <h3>📊 Variable Table</h3> <p>Define all PLC variables you want to access. Each variable requires:</p> <ul> <li><strong>Name:</strong> Unique identifier for the variable (used in Node-RED flows)</li> <li><strong>Address:</strong> PLC memory address using the format below</li> </ul> <h4>📍 Address Format Quick Reference</h4> <table style="border-collapse: collapse; width: 100%; font-size: 12px;"> <tr style="background-color: #f0f0f0;"><th style="border: 1px solid #ddd; padding: 4px;">Type</th><th style="border: 1px solid #ddd; padding: 4px;">Format</th><th style="border: 1px solid #ddd; padding: 4px;">Example</th><th style="border: 1px solid #ddd; padding: 4px;">Description</th></tr> <tr><td style="border: 1px solid #ddd; padding: 4px;"><strong>Boolean</strong></td><td style="border: 1px solid #ddd; padding: 4px;"><code>DB{n},X{offset}.{bit}</code></td><td style="border: 1px solid #ddd; padding: 4px;"><code>DB1,X0.0</code></td><td style="border: 1px solid #ddd; padding: 4px;">Data Block 1, Byte 0, Bit 0</td></tr> <tr><td style="border: 1px solid #ddd; padding: 4px;"><strong>Integer</strong></td><td style="border: 1px solid #ddd; padding: 4px;"><code>DB{n},INT{offset}</code></td><td style="border: 1px solid #ddd; padding: 4px;"><code>DB1,INT2</code></td><td style="border: 1px solid #ddd; padding: 4px;">16-bit signed integer at byte 2</td></tr> <tr><td style="border: 1px solid #ddd; padding: 4px;"><strong>Real</strong></td><td style="border: 1px solid #ddd; padding: 4px;"><code>DB{n},REAL{offset}</code></td><td style="border: 1px solid #ddd; padding: 4px;"><code>DB1,REAL4</code></td><td style="border: 1px solid #ddd; padding: 4px;">32-bit float at byte 4</td></tr> <tr><td style="border: 1px solid #ddd; padding: 4px;"><strong>Double Int</strong></td><td style="border: 1px solid #ddd; padding: 4px;"><code>DB{n},DINT{offset}</code></td><td style="border: 1px solid #ddd; padding: 4px;"><code>DB1,DINT8</code></td><td style="border: 1px solid #ddd; padding: 4px;">32-bit signed integer</td></tr> <tr><td style="border: 1px solid #ddd; padding: 4px;"><strong>Input</strong></td><td style="border: 1px solid #ddd; padding: 4px;"><code>I{byte}.{bit}</code></td><td style="border: 1px solid #ddd; padding: 4px;"><code>I0.0</code></td><td style="border: 1px solid #ddd; padding: 4px;">Digital input</td></tr> <tr><td style="border: 1px solid #ddd; padding: 4px;"><strong>Output</strong></td><td style="border: 1px solid #ddd; padding: 4px;"><code>Q{byte}.{bit}</code></td><td style="border: 1px solid #ddd; padding: 4px;"><code>Q0.1</code></td><td style="border: 1px solid #ddd; padding: 4px;">Digital output</td></tr> <tr><td style="border: 1px solid #ddd; padding: 4px;"><strong>Memory</strong></td><td style="border: 1px solid #ddd; padding: 4px;"><code>M{byte}.{bit}</code></td><td style="border: 1px solid #ddd; padding: 4px;"><code>M10.0</code></td><td style="border: 1px solid #ddd; padding: 4px;">Memory bit</td></tr> </table> <h3>🔧 PLC Setup Requirements</h3> <h4>S7-1200/1500 (TIA Portal)</h4> <ol> <li>Disable <strong>"Optimized block access"</strong> for data blocks you want to access</li> <li>Enable <strong>"Permit access with PUT/GET communication"</strong> in CPU Protection settings</li> <li>Configure Ethernet interface with static IP address</li> <li>Ensure data blocks are not write-protected</li> </ol> <h4>S7-300/400 (STEP 7 Classic)</h4> <ol> <li>Data blocks should be created as non-optimized</li> <li>Enable communication in hardware configuration</li> <li>Configure Ethernet communication processor (CP343/CP443)</li> <li>Set appropriate connection resources</li> </ol> <h3>🚦 Status Indicators</h3> <ul> <li><span style="color: green; font-size: 16px;">●</span> <strong>Online:</strong> Successfully connected and communicating</li> <li><span style="color: yellow; font-size: 16px;">●</span> <strong>Connecting:</strong> Attempting to establish connection</li> <li><span style="color: red; font-size: 16px;">●</span> <strong>Offline:</strong> Connection failed or lost</li> <li><span style="color: orange; font-size: 16px;">●</span> <strong>Bad Values:</strong> Connected but some variables have invalid addresses</li> </ul> <h3>🛠️ Troubleshooting</h3> <dl class="message-properties"> <dt>Connection timeout</dt> <dd>• Check IP address and network connectivity<br>• Verify PLC is powered and accessible<br>• Check firewall settings on both sides</dd> <dt>Variable errors</dt> <dd>• Verify address format matches your PLC memory layout<br>• Check data block existence in PLC<br>• Ensure correct data type alignment</dd> <dt>Permission denied</dt> <dd>• Enable PUT/GET access in PLC configuration<br>• Check data block access rights<br>• Verify rack/slot numbers</dd> <dt>Intermittent connection</dt> <dd>• Increase timeout value<br>• Check network stability<br>• Reduce cycle time if too aggressive</dd> </dl> <h3>📈 Performance Tips</h3> <ul> <li><strong>Optimize cycle time:</strong> Balance between responsiveness and network load</li> <li><strong>Group variables:</strong> Place related variables in the same data block for better performance</li> <li><strong>Limit variable count:</strong> Too many variables can impact performance</li> <li><strong>Use appropriate data types:</strong> Match PLC data types exactly to avoid conversion overhead</li> </ul> <h3>🔗 References & Examples</h3> <p>📖 <strong>Complete Documentation:</strong> <a href="https://github.com/oriolrius/node-red-contrib-s7/blob/main/EXAMPLE-USAGE.md" target="_blank">GitHub Usage Guide</a></p> <p>📋 <strong>Example Flow:</strong> <a href="https://github.com/oriolrius/node-red-contrib-s7/blob/main/example-flow.json" target="_blank">Sample Flow JSON</a></p> <p>🔧 <strong>Issues & Support:</strong> <a href="https://github.com/oriolrius/node-red-contrib-s7/issues" target="_blank">GitHub Issues</a></p> <p>📚 <strong>Full Address Reference:</strong> See detailed address table in complete documentation</p> <h3>⚠️ Important Notes</h3> <ul> <li>This configuration node is shared among all S7 nodes in your flow</li> <li>Changes to the endpoint configuration affect all connected nodes</li> <li>Always test with non-critical PLC systems first</li> <li>Monitor PLC CPU usage when using aggressive cycle times</li> <li>Use a dedicated VLAN for PLC communication in production environments</li> </ul> </script> <script type="text/javascript"> /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ var saveAs = saveAs || function (e) { "use strict"; if (typeof e === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) { return } var t = e.document, n = function () { return e.URL || e.webkitURL || e }, r = t.createElementNS("http://www.w3.org/1999/xhtml", "a"), o = "download" in r, a = function (e) { var t = new MouseEvent("click"); e.dispatchEvent(t) }, i = /constructor/i.test(e.HTMLElement) || e.safari, f = /CriOS\/[\d]+/.test(navigator.userAgent), u = function (t) { (e.setImmediate || e.setTimeout)(function () { throw t }, 0) }, s = "application/octet-stream", d = 1e3 * 40, c = function (e) { var t = function () { if (typeof e === "string") { n().revokeObjectURL(e) } else { e.remove() } }; setTimeout(t, d) }, l = function (e, t, n) { t = [].concat(t); var r = t.length; while (r--) { var o = e["on" + t[r]]; if (typeof o === "function") { try { o.call(e, n || e) } catch (a) { u(a) } } } }, p = function (e) { if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(e.type)) { return new Blob([String.fromCharCode(65279), e], { type: e.type }) } return e }, v = function (t, u, d) { if (!d) { t = p(t) } var v = this, w = t.type, m = w === s, y, h = function () { l(v, "writestart progress write writeend".split(" ")) }, S = function () { if ((f || m && i) && e.FileReader) { var r = new FileReader; r.onloadend = function () { var t = f ? r.result : r.result.replace(/^data:[^;]*;/, "data:attachment/file;"); var n = e.open(t, "_blank"); if (!n) e.location.href = t; t = undefined; v.readyState = v.DONE; h() }; r.readAsDataURL(t); v.readyState = v.INIT; return } if (!y) { y = n().createObjectURL(t) } if (m) { e.location.href = y } else { var o = e.open(y, "_blank"); if (!o) { e.location.href = y } } v.readyState = v.DONE; h(); c(y) }; v.readyState = v.INIT; if (o) { y = n().createObjectURL(t); setTimeout(function () { r.href = y; r.download = u; a(r); h(); c(y); v.readyState = v.DONE }); return } S() }, w = v.prototype, m = function (e, t, n) { return new v(e, t || e.name || "download", n) }; if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) { return function (e, t, n) { t = t || e.name || "download"; if (!n) { e = p(e) } return navigator.msSaveOrOpenBlob(e, t) } } w.abort = function () { }; w.readyState = w.INIT = 0; w.WRITING = 1; w.DONE = 2; w.error = w.onwritestart = w.onprogress = w.onwrite = w.onabort = w.onerror = w.onwriteend = null; return m }(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content); if (typeof module !== "undefined" && module.exports) { module.exports.saveAs = saveAs } else if (typeof define !== "undefined" && define !== null && define.amd !== null) { define("FileSaver.js", function () { return saveAs }) } </script> <script type="text/javascript"> function validateS7Address(address) { if (!address) return 'ERR_PARSE_EMPTY'; var NODES7_REGEX_ADDR = /^(?:DB(\d+),)?([A-Z]+)(\d+)(?:\.(\d+))?(?:\.(\d+))?$/; var NODES7_ADDRTYPE = ["E", "I", "A", "Q", "P", "M", "T", "C"]; var NODES7_DATATYPE = ["X", "B", "C", "I", "DI", "W", "D", "DW", "R", "RI", "RDI", "RW", "RD", "RDW", "RR"]; var NODES7_DATATYPE_DB = ["X", "BYTE", "CHAR", "STRING", "INT", "DINT", "WORD", "DWORD", "REAL", "RINT", "RDINT", "RWORD", "RDWORD", "RREAL", "DT", "DTZ", "DTL", "DTLZ", "B", "C", "S", "I", "DI", "W", "D", "DW", "R", "RI", "RDI", "RW", "RD", "RDW", "RR"]; var match = address.match(NODES7_REGEX_ADDR); if (!match) return 'ERR_PARSE_UNKNOWN_FORMAT'; var match_db = match[1]; var match_area = match[2]; var match_addr = match[3]; var match_bitAddr = parseInt(match[4]); var match_arrLen = parseInt(match[5]); var addrType, dataType, addressOffset, dbNumber; addressOffset = parseInt(match_addr) if (isNaN(addressOffset)) return 'ERR_PARSE_ADDR_OFFSET'; if (match_db) { dbNumber = parseInt(match_db); if (dbNumber < 1 || isNaN(dbNumber)) return 'ERR_PARSE_DB_NUMBER'; } if (dbNumber) { if (!NODES7_DATATYPE_DB.includes(match_area)) return 'ERR_PARSE_DB_DATATYPE'; dataType = match_area; } else { // the first char indicates the PLC area (I, Q, M, P, ...) addrType = match_area.charAt(0); // the data type (if area is P, skip the first char (I,E,Q,A)) dataType = match_area.substr(addrType === "P" ? 2 : 1); // validate address type if (!NODES7_ADDRTYPE.includes(addrType)) return 'ERR_PARSE_AREA'; if (dataType === "" && addrType !== "T" && addrType !== "C") { dataType = "X"; } // validate data type if (!NODES7_DATATYPE.includes(dataType)) return 'ERR_PARSE_DATATYPE'; } if (dataType === "X") { // handle array lengths and bit address if (isNaN(match_bitAddr)) return 'ERR_PARSE_BIT_OFFSET'; if (match_bitAddr > 7) return 'ERR_PARSE_BIT_OFFSET'; } else if (dataType === "STRING" || dataType === "S") { // match_bitAddr is the string length for string types if (isNaN(match_bitAddr) || match_bitAddr < 1) return 'ERR_PARSE_STRING_LEN'; } else { // the array length should be at the bitAddr field, this is a syntax error if (!isNaN(match_arrLen)) return 'ERR_PARSE_INVALID_BIT_OFFSET'; // the bitAddr field is actually the array length if (!isNaN(match_bitAddr) && match_bitAddr < 1) return 'ERR_PARSE_INVALID_ARR_LEN'; } return null; } function validateAddressList(list) { for (var i = 0; i < list.length; i++){ var elm = list[i]; if (!elm.name) return false; if (validateS7Address(elm.addr)) return false; } return true; } function validateTSAP(num) { num = num.toString(); if (num.length != 2) return false; if (!(/^[0-9a-fA-F]+$/.test(num))) return false; var i = parseInt(num, 16); if (isNaN(i) || i < 0 || i > 0xff) return false; return true; } RED.nodes.registerType('s7 endpoint', { category: 'config', defaults: { transport: { value: "iso-on-tcp" }, address: { value: "", validate: function(v) {return this.transport == 'iso-on-tcp' ? !!v : true} }, port: { value: "102", validate: RED.validators.number() }, rack: { value: "0", validate: RED.validators.number() }, slot: { value: "1", validate: RED.validators.number() }, localtsaphi: { value: "01", validate: validateTSAP }, localtsaplo: { value: "00", validate: validateTSAP }, remotetsaphi: { value: "01", validate: validateTSAP }, remotetsaplo: { value: "00", validate: validateTSAP }, connmode: { value: "rack-slot" }, adapter: { value: "", type: "mpi-s7 adapter", validate: function(v) {return this.transport != "mpi-s7" || !!v } }, busaddr: { value: 2 }, cycletime: { value: 1000 }, timeout: { value: 2000 }, name: { value: "" }, rewritetimes: { value: 0 }, rewriteinterval: { value: 500 }, vartable: { value: [{ name: "", addr: "" }], validate: validateAddressList } }, label: function () { if (this.name) { return this.name } else if (this.transport == "iso-on-tcp") { return this.address + ":" + this.port + "@" + ( this.connmode == 'tsap' ? (this.localtsaphi + "." + this.localtsaplo + "/" + this.remotetsaphi + "." + this.remotetsaplo) : (this.rack + ":" + this.slot)) } else if (this.transport == "mpi-s7") { var adapterNode = RED.nodes.node(this.adapter); if (adapterNode) { return adapterNode.label() + "/" + this.busaddr; } return "MPI/PPI/DP @ ??/" + this.busaddr; } else { return "s7 endpoint"; } }, oneditprepare: function () { var self = this; var tt = this._.bind(this); var labelName = this._("s7.endpoint.label.variables.name"); var labelAddr = this._("s7.endpoint.label.variables.addr"); var labelDel = this._("s7.endpoint.label.variables.del"); var connTypeList = $('#node-config-input-connmode'); var rackSlotRow = $('#node-config-s7-endpoint-mode-rackslot'); var tsapRow = $('#node-config-s7-endpoint-mode-tsap'); var transportList = $('#node-config-input-transport'); var transportIsoOnTCPGroup = $('#s7-endpoint-transport-iso-on-tcp'); var transportMpiS7Group = $('#s7-endpoint-transport-mpi-s7'); var fieldAddressIsoOnTcp = $('#node-config-input-address'); var fieldName = $('#node-config-input-name'); var lookupPlcPNBtn = $('#node-config-lookup-plc-pn'); var flashPlcPNBtn = $('#node-config-flash-plc-pn'); // put a placeholder for mpi-s7 adapter if not installed if (!RED.nodes.getType('mpi-s7 adapter')){ $('#node-config-input-adapter').hide(); $('#s7-endpoint-adapter-placeholder').show(); } // Prepare tabs var tabs = RED.tabs.create({ id: "node-config-s7-endpoint-tabs", onchange: function (tab) { $("#node-config-s7-endpoint-tabs-content").children().hide(); $("#" + tab.id).show(); } }); tabs.addTab({ id: "s7-endpoint-tab-connection", label: this._("s7.endpoint.label.tabs.connection") }); tabs.addTab({ id: "s7-endpoint-tab-variables", label: this._("s7.endpoint.label.tabs.variables") }); setTimeout(function () { tabs.resize() }, 0); function generateVariable(variable) { var curTooltip; var previousValue = variable.addr; var container = $('<li/>', { style: "background: #fff; margin:0; padding:8px 0px; border-bottom: 1px solid #ccc;" }); var row1 = $('<div/>').appendTo(container); var variableAddr = $('<input/>', { style: "width: 110px; margin-right: 10px;", class: "node-config-input-variable-addr", type: "text", placeholder: labelAddr }).appendTo(row1); var variableName = $('<input/>', { style: "width: 250px", class: "node-config-input-variable-name", type: "text", placeholder: labelName }).appendTo(row1); var finalspan = $('<span/>', { style: "float: right; margin-right: 10px;" }).appendTo(row1); var deleteButton = $('<a/>', { href: "#", class: "editor-button editor-button-small", style: "margin-top: 7px; margin-left: 5px;", title: labelDel }).appendTo(finalspan); $('<i/>', { class: "fa fa-remove" }).appendTo(deleteButton); deleteButton.click(function () { container.css({ "background": "#fee" }); container.fadeOut(150, function () { $(this).remove(); }); if (curTooltip) curTooltip.close(); }); variableAddr.change(function () { //validate address var curVal = variableAddr.val(); var valError = validateS7Address(curVal); if (valError) { variableAddr.addClass('input-error') var errorText = tt("s7.validation." + valError); if (curTooltip) { curTooltip.setContent(errorText); curTooltip.open(); } else if (RED.popover && RED.popover.tooltip){ curTooltip = RED.popover.tooltip(variableAddr, errorText); curTooltip.open(); } } else { variableAddr.removeClass('input-error'); if(curTooltip) { curTooltip.close(); curTooltip.setContent(''); //hack to remove the popup, as Node-RED don't offer // and "unbind" function. May break in the future variableAddr.off('mouseenter mouseleave disabled'); curTooltip = null; } } //update name if matching old one if (previousValue && variableName.val() == previousValue) { variableName.val(curVal); } previousValue = curVal; }); //populate data variableAddr.val(variable.addr); variableName.val(variable.name); variableAddr.change(); $("#node-config-input-variables-container").append(container); } function cleanVarTable() { $("#node-config-input-variables-container").children().remove(); } function populateVarTable() { if (self.vartable) { if (typeof self.vartable == 'string') { self.vartable = JSON.parse(self.vartable); } for (var i = 0; i < self.vartable.length; i++) { generateVariable(self.vartable[i]); } } } $("#node-config-input-add-variable").click(function () { generateVariable({ name: "", addr: "" }); }); $("#node-config-input-rewritetimes").spinner({ min: 0 }); $("#node-config-input-rewriteinterval").spinner({ min: 0 }); $("#node-config-input-cycletime").spinner({ min: 0 }); $("#node-config-input-timeout").spinner({ min: 500 }); $("#node-config-input-busaddr").spinner({ min: 0, max: 127 }); $("#node-config-s7-endpoint-var-clean").click(cleanVarTable); populateVarTable(); // export function exportCSV() { var vars = $("#node-config-input-variables-container").children(); var lines = []; vars.each(function (i) { var elm = $(this); lines.push([ elm.find(".node-config-input-variable-addr").val(), //addr elm.find(".node-config-input-variable-name").val() //name ].join(';')); }); saveAs(new Blob([lines.join('\r\n')]), 's7endpoint' + (self.name ? '_' + self.name : '') + '.csv'); } $('#node-config-s7-endpoint-var-export').click(exportCSV); // import function importCSV(e) { var file = e.target.files[0]; if (!file) { return; } var reader = new FileReader(); reader.onload = function (e) { var res = [], i, fields; var contents = e.target.result || ''; var lines = contents.split(/[\r\n]+/); if (!lines.length) { alert('file is empty!'); return; } for (i = 0; i < lines.length; i++) { lines[i] = lines[i].trim(); if (lines[i] == '') continue; fields = lines[i].split(/[\t;]/); if (fields.length < 2) { alert('line must have at least two parameters, address and name'); return; } res.push({ addr: fields[0], name: fields[1] }); } if (res.length) { cleanVarTable(); self.vartable = res; populateVarTable(); } }; reader.readAsText(file); } $('#node-config-s7-endpoint-var-import').on('change', importCSV); $('#node-config-s7-endpoint-var-import-btn').click(function () { $('#node-config-s7-endpoint-var-import').click(); }) //set defaults for old flows if (self.connmode === undefined) { console.log('defaults conmode to "rack-slot"'); self.connmode = 'rack-slot'; self.localtsaphi = "01"; self.localtsaplo = "00"; self.remotetsaphi = "01"; self.remotetsaplo = "00"; $("#node-config-input-localtsaphi").val(self.localtsaphi); $("#node-config-input-localtsaplo").val(self.localtsaplo); $("#node-config-input-remotetsaphi").val(self.remotetsaphi); $("#node-config-input-remotetsaplo").val(self.remotetsaplo); connTypeList.val(self.connmode); } if (self.rewritetimes === undefined) { self.rewritetimes = 0; $("#node-config-input-rewritetimes").val(self.rewritetimes); } if (self.rewriteinterval === undefined) { self.rewriteinterval = 500; $("#node-config-input-rewriteinterval").val(self.rewriteinterval); } if (self.timeout === undefined) { self.timeout = 2000; $("#node-config-input-timeout").val(self.timeout); } if (self.verbose === undefined) { self.verbose = 'default'; $("#node-config-input-verbose").val(self.verbose); } if (self.transport === undefined) { self.transport = 'iso-on-tcp'; transportList.val(self.transport); } connTypeList.change(function () { if (connTypeList.val() == "rack-slot") { rackSlotRow.show(); tsapRow.hide(); } else if (connTypeList.val() == "tsap") { rackSlotRow.hide(); tsapRow.show(); } else { rackSlotRow.hide(); tsapRow.hide(); } }); connTypeList.change(); transportList.change(function() { var curVal = transportList.val(); if(curVal == "iso-on-tcp") { transportIsoOnTCPGroup.show(); transportMpiS7Group.hide(); } else if (curVal == "mpi-s7") { transportIsoOnTCPGroup.hide(); transportMpiS7Group.show(); } else { transportIsoOnTCPGroup.hide(); transportMpiS7Group.hide(); } }); transportList.change(); $.getJSON('__node-red-contrib-s7/discover/available/iso-on-tcp', function(data) { if (data) { lookupPlcPNBtn.removeClass('disabled'); } }); var lookupPlcPNValue = null; fieldAddressIsoOnTcp.change(function() { lookupPlcPNValue = null; flashPlcPNBtn.addClass('disabled'); }) lookupPlcPNBtn.click(function () { if (lookupPlcPNBtn.hasClass('disabled')) return; lookupPlcPNBtn.addClass('disabled'); var iconLookup = lookupPlcPNBtn.children('i'); iconLookup.removeClass('fa-search').addClass('fa-spinner fa-spin fa-fw'); $.getJSON('__node-red-contrib-s7/discover/iso-on-tcp', function (data) { console.log("node-red-contrib-s7 discovered PLCs", data) var listData = $.map(data, function(val){ return { label: val, value: val['IP Address']} }) fieldAddressIsoOnTcp.autocomplete({ source: listData, minLength: 0, create: function (event, ui) { $(this).data('ui-autocomplete')._renderItem = function (ul, elm) { var item = elm.label; elmHtml = '<a>' + '<small style="float: right;">' + item['Vendor Value'] + '</small>' + '<b>' + item['Station Name'] + '</b>' + '<br/>' + '<small style="float: right;">' + item['MAC Address'] + '</small>' + item['IP Address'] + '</a>' return $('<li style="padding: 2px">').attr("data-value", elm.value).append(elmHtml).appendTo(ul); }; }, select: function (event, ui) { var elm = ui.item.label event.preventDefault(); event.stopPropagation(); fieldAddressIsoOnTcp.val(elm['IP Address']); fieldAddressIsoOnTcp.change(); if (!fieldName.val()){ fieldName.val(elm['Station Name']); } lookupPlcPNValue = elm; flashPlcPNBtn.removeClass('disabled'); } }); fieldAddressIsoOnTcp.focus(function() { fieldAddressIsoOnTcp.autocomplete("search", "") }); fieldAddressIsoOnTcp.focus(); }).always(function() { iconLookup.removeClass('fa-spinner fa-spin fa-fw').addClass('fa-search'); lookupPlcPNBtn.removeClass('disabled'); }); }); flashPlcPNBtn.click(function() { if (flashPlcPNBtn.hasClass('disabled')) return; if (!lookupPlcPNValue) { flashPlcPNBtn.addClass('disabled'); return; } var macaddr = lookupPlcPNValue['MAC Address'].replace(/:/g, '-'); var tgtUrl = '__node-red-contrib-s7/flashled/iso-on-tcp/' + macaddr; $.getJSON(tgtUrl); }); }, oneditsave: function () { var node = this; var vars = $("#node-config-input-variables-container").children(); node.vartable = []; vars.each(function (i) { var elm = $(this); var addr = elm.find(".node-config-input-variable-addr").val(); var name = elm.find(".node-config-input-variable-name").val(); var v = { addr: addr, name: name || addr } node.vartable.push(v); }); } }); </script> <!-- ######################################################################################## --> <script type="text/x-red" data-template-name="s7 in"> <div class="form-row"> <label for="node-input-endpoint"><i class="fa fa-bolt"></i> <span data-i18n="s7.in.label.endpoint"></span></label> <input type="text" id="node-input-endpoint" data-i18n="[placeholder]s7.in.label.endpoint"> </div> <div class="form-row"> <label for="node-input-mode"><i class="fa fa-sliders"></i> <span data-i18n="s7.in.label.mode"></span></label> <select type="text" id="node-input-mode"> <option value="single" data-i18n="s7.in.mode.single"></option> <option value="all-split" data-i18n="s7.in.mode.all-split"></option> <option value="all" data-i18n="s7.in.mode.all"></option> </select> </div> <div class="form-row s7-input-var-row"> <label for="node-input-variable"><i class="fa fa-random"></i> <span data-i18n="s7.in.label.variable"></span></label> <select type="text" id="node-input-variable"> </select> <span id="s7-custom-var-addr"></span> </div> <div class="form-row"> <label>&nbsp;</label> <input type="checkbox" id="node-input-diff" style="display: inline-block; width: auto; vertical-align: top;"> <label for="node-input-diff" style="width:70%;"><span data-i18n="s7.in.label.diff"></span></label> </div> <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="s7.label.name"></span></label> <input type="text" id="node-input-name" data-i18n="[placeholder]s7.label.name"> </div> </script> <script type="text/x-red" data-help-name="s7 in"> <p>📥 <strong>Reads data from a Siemens S7 PLC</strong> cyclically or on-demand, sending messages when values are received or change.</p> <h3>📤 Outputs</h3> <dl class="message-properties"> <dt>payload<span class="property-type">any</span></dt> <dd>The value(s) read from the PLC. Format depends on the selected mode (single value, object, or individual messages)</dd> <dt>topic<span class="property-type">string</span></dt> <dd>The variable name when mode is "Single Variable" or "All variables, one per message"</dd> <dt>_s7<span class="property-type">object</span></dt> <dd>Contains PLC connection metadata: <ul> <li><code>plc</code> - PLC endpoint name</li> <li><code>ip</code> - PLC IP address</li> <li><code>status</code> - Connection status (online/offline)</li> <li><code>time</code> - Timestamp of reading</li> </ul> </dd> </dl> <h3>⚙️ Configuration</h3> <h4>📊 Reading Modes</h4> <dl class="message-properties"> <dt>Single Variable<span class="property-type">focused</span></dt> <dd> <strong>Use case:</strong> Monitor one specific variable<br> <strong>Output:</strong> <code>msg.payload</code> = variable value, <code>msg.topic</code> = variable name<br> <strong>Example:</strong> <code>{payload: true, topic: "motor_running"}</code> </dd> <dt>All Variables<span class="property-type">comprehensive</span></dt> <dd> <strong>Use case:</strong> Get all variables in one message<br> <strong>Output:</strong> <code>msg.payload</code> = object with all variables<br> <strong>Example:</strong> <code>{payload: {motor_running: true, temperature: 42.5}}</code> </dd> <dt>All Variables, One per Message<span class="property-type">individual</span></dt> <dd> <strong>Use case:</strong> Process each variable separately<br> <strong>Output:</strong> Separate message for each variable<br> <strong>Example:</strong> Multiple messages: <code>{payload: true, topic: "motor_running"}</code>, <code>{payload: 42.5, topic: "temperature"}</code> </dd> </dl> <h4>🔄 Trigger Options</h4> <dl class="message-properties"> <dt>Send on Change Only (diff)<span class="property-type">boolean</span></dt> <dd> <strong>Checked:</strong> Send messages only when values change (recommended for most cases)<br> <strong>Unchecked:</strong> Send messages every cycle regardless of value changes </dd> </dl> <h3>💡 Usage Examples</h3> <h4>Monitor Single Variable</h4> <pre style="background-color: #f5f5f5; padding: 8px; border-radius: 4px; font-size: 12px;"> Configuration: • Mode: Single Variable • Variable: motor_running • Diff: ✓ (checked) Output when motor starts: { "payload": true, "topic": "motor_running", "_s7": { "plc": "Factory PLC", "ip": "192.168.1.10", "status": "online", "time": "2025-06-17T17:00:00.000Z" } }</pre> <h4>Get All Variables</h4> <pre style="background-color: #f5f5f5; padding: 8px; border-radius: 4px; font-size: 12px;"> Configuration: • Mode: All Variables • Diff: ✓ (checked) Output when any variable changes: { "payload": { "motor_running": true, "temperature": 42.5, "speed_setpoint": 75.0, "alarm_active": false }, "_s7": { ... } }</pre> <h4>Process Variables Individually</h4> <pre style="background-color: #f5f5f5; padding: 8px; border-radius: 4px; font-size: 12px;"> Configuration: • Mode: All Variables, One per Message • Diff: ✓ (checked) Output (separate messages): {payload: true, topic: "motor_running", "_s7": {...}} {payload: 42.5, topic: "temperature", "_s7": {...}} {payload: 75.0, topic: "speed_setpoint", "_s7": {...}}</pre> <h3>🎯 Best Practices</h3> <ul> <li><strong>Use "diff" mode:</strong> Reduces message traffic and processing load</li> <li><strong>Single Variable mode:</strong> Best for critical monitoring or triggering specific actions</li> <li><strong>All Variables mode:</strong> Ideal for dashboards or data logging</li> <li><strong>One per Message mode:</strong> Good for individual processing but watch message volume</li> <li><strong>Filter downstream:</strong> Use Switch nodes to filter specific conditions</li> </ul> <h3>🚦 Status Display</h3> <ul> <li><span style="color: green; font-size: 14px;">●</span> <strong>Online + value:</strong> Connected and last read value</li> <li><span style="color: yellow; font-size: 14px;">●</span> <strong>Connecting:</strong> Attempting to connect to PLC</li> <li><span style="color: red; font-size: 14px;">●</span> <strong>Offline:</strong> No connection to PLC</li> <li><span style="color: orange; font-size: 14px;">●</span> <strong>Bad Values:</strong> Some variables have invalid addresses</li> </ul> <h3>⚡ Performance Considerations</h3> <ul> <li><strong>Message volume:</strong> "One per message" mode can generate many messages - monitor flow performance</li> <li><strong>Cycle time:</strong> Faster cycles = more responsive but higher CPU/network load</li> <li><strong>Variable count:</strong> More variables = longer read cycles</li> <li><strong>Network latency:</strong> Adjust timeout if experiencing intermittent failures</li> </ul> <h3>🔧 Troubleshooting</h3> <dl class="message-properties"> <dt>No messages received</dt> <dd>• Check PLC connection status<br>• Verify variables are defined in endpoint<br>• Ensure diff mode isn't preventing initial messages</dd> <dt>Outdated values</dt> <dd>• Check cycle time setting<br>• Verify PLC is updating the variables<br>• Check for communication errors</dd> <dt>Too many messages</dt> <dd>• Enable diff mode to reduce message frequency<br>• Consider using "All Variables" instead of "One per Message"<br>• Increase cycle time if appropriate</dd> </dl> <h3>🔗 Related Nodes</h3> <ul> <li><strong>S7 Endpoint:</strong> Configure PLC connection and variables</li> <li><strong>S7 Out:</strong> Write data to PLC</li> <li><strong>S7 Control:</strong> Control reading behavior dynamically</li> <li><strong>Switch:</strong> Filter messages based on topic or payload values</li> <li><strong>Change:</strong> Transform or extract specific values from messages</li> </ul> <h3>📖 References</h3> <p>📚 <strong>Complete Guide:</strong> <a href="https://github.com/oriolrius/node-red-contrib-s7/blob/main/EXAMPLE-USAGE.md" target="_blank">GitHub Documentation</a></p> <p>📋 <strong>Example Flow:</strong> <a href="https://github.com/oriolrius/node-red-contrib-s7/blob/main/example-flow.json" target="_blank">Sample Implementation</a></p> </script> <script type="text/javascript"> RED.nodes.registerType('s7 in', { category: 'plc', defaults: { endpoint: { value: "", type: "s7 endpoint", required: true }, mode: { value: "single" }, variable: { value: "" }, diff: { value: true }, name: { value: "" } }, color: "#3FADB5", inputs: 0, outputs: 1, icon: "serial.png", label: function () { var self = this; if (this.name) return this.name; var endpointNode = RED.nodes.node(this.endpoint); if (endpointNode) { return (this.mode == 'single' ? this.variable : null) || endpointNode.label(); } return this._("s7.in.label.name"); }, labelStyle: function () { return this.name ? "node_label_italic" : ""; }, oneditprepare: function () { var self = this; var varList = $('#node-input-variable'); var varAddr = $('#s7-custom-var-addr'); var modeList = $('#node-input-mode'); var endpointList = $("#node-input-endpoint"); var vars = []; function updateVarList(endpointId) { $('#node-input-variable option').remove(); var endpointNode = RED.nodes.node(endpointId); if (!endpointNode) return; vars = endpointNode.vartable || []; if (typeof vars === 'string') vars = JSON.parse(vars); varList.append($('<option/>', { disabled: "disabled", selected: "selected", style: "display:none;", text: vars.length ? self._("s7.in.label.variable-select") : self._("s7.in.label.variable-novar") })); $.each(vars, function (i, val) { varList.append($('<option/>', { value: val.name || val.addr, text: val.name || val.addr })); if (val.name == self.variable) { varList.val(self.variable); } }); } varList.change(function () { $.each(vars, function (i, val) { if (varList.val() == val.name) { varAddr[0].innerText = val.addr; return true; } }); }); endpointList.change(function () { updateVarList(endpointList.val()); }); updateVarList(self.endpoint); modeList.change(function () { if (modeList.val() == "single") { varList.parent().show(); } else { varList.parent().hide(); } }); modeList.change(); } }); </script> <!-- ######################################################################################## --> <script type="text/x-red" data-template-name="s7 out"> <div class="form-row"> <label for="node-input-endpoint"><i class="fa fa-bolt"></i> <span data-i18n="s7.out.label.endpoint"></span></label> <input type="text" id="node-input-endpoint" data-i18n="[placeholder]s7.out.label.endpoint"> </div> <div class="form-row s7-input-var-row"> <label for="node-input-variable"><i class="fa fa-random"></i> <span data-i18n="s7.out.label.variable"></span></label> <select type="text" id="node-input-variable"> </select> <span id="s7-custom-var-addr"></span> </div> <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="s7.label.name"></span></label> <input type="text" id="node-input-name" data-i18n="[placeholder]s7.label.name"> </div> <div class="form-tips"><span data-i18n="[html]s7.out.disclaimer"></span></div> </script> <script type="text/x-red" data-help-name="s7 out"> <p>📤 <strong>Writes data to a Siemens S7 PLC</strong> memory area with automatic type conversion and error handling.</p> <div style="background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 4px; padding: 8px; margin: 8px 0;"> <strong>⚠️ Production Safety Warning</strong><br/> Always test thoroughly on non-production systems first! Writing incorrect values to PLCs can cause equipment damage, safety hazards, or production interruptions. The authors are not liable for any damage or injury caused by the use of this node. </div> <h3>📥 Inputs</h3> <dl class="message-properties"> <dt>payload<span class="property-type">any</span></dt> <dd> <strong>Required:</strong> The data to write to the PLC. Automatically converted to match the target variable's data type. <ul> <li><strong>Boolean:</strong> <code>true</code>/<code>false</code>, <code>1</code>/<code>0</code>, <code>"true"</code>/<code>"false"</code></li> <li><strong>Numbers:</strong> Integers, floats, or numeric strings</li> <li><strong>Arrays:</strong> For writing multiple variables (must match <code>msg.variable</code> array length)</li> </ul> </dd> <dt>variable<span class="property-type">string | array</span></dt> <dd> <strong>Optional:</strong> Override the configured variable name(s). Use this for dynamic variable selection. <ul> <li><strong>Single:</strong> <code>"motor_running"</code></li> <li><strong>Multiple:</strong> <code>["motor_running", "speed_setpoint"]</code></li> </ul> </dd> </dl> <h3>📤 Outputs</h3> <dl class="message-properties"> <dt>payload<span class="property-type">object</span></dt> <dd> Write operation result containing: <ul> <li><code>variable</code> - Variable name(s) written</li> <li><code>payload</code> - Value(s) written</li> <li><code>values</code> - Key-value pairs of written data</li> <li><code>newValues</code> - PLC values after write (if verification enabled)</li> <li><code>wrongValues</code> - Values that don't match written data</li> <li><code>bingo</code> - Boolean indicating successful verification</li> <li><code>error</code> - Error message if write failed</li> <li><code>rewriteCount</code> - Number of retry attempts performed</li> </ul> </dd> <dt>_s7<span class="property-type">object</span></dt> <dd>PLC connection metadata (same as S7 In node)</dd> </dl> <h3>⚙️ Configuration Options</h3> <h4>Variable Selection</h4> <dl class="message-properties"> <dt>Fixed Variable<span class="property-type">static</span></dt> <dd>Select a specific variable from the endpoint's variable table. Simple and reliable for dedicated write operations.</dd> <dt>Dynamic Variable<span class="property-type">flexible</span></dt> <dd>Leave variable field empty and specify <code>msg.variable</code> in the message. Useful for conditional or user-dri