Describe the bug
S7ItemGroup._prepareReadPackets() builds read packets by summing the raw
part.length values against maxPayloadSize = pduSize - 18. However, in the
S7 protocol every data item in a read response is padded to an even number
of bytes (fill byte), except the last one. The optimizer does not account for
these fill bytes.
When a packet contains many odd-length parts, the actual response can exceed
the negotiated PDU size even though the optimizer's own accounting says it
fits. The PLC then rejects the request with PLC error [0x8500] (L7 PDU size
error) on every read cycle, and the connection never recovers.
With pduSize = 960 the implicit safety margin is 18 bytes, so 19 or more
odd-length parts in the same packet are enough to overflow.
A nasty property of this bug: removing any single variable from the table
reshuffles the greedy bin-packing and usually makes the error disappear, which
makes it look random and very hard to diagnose in the field.
This may also be the root cause of #-related reports of
S7 protocol error: Wrong frames with large variable tables (e.g.
st-one-io/node-red-contrib-s7#134).
To Reproduce
Self-contained script, no PLC needed (only the packet preparation is
exercised):
// repro.js - npm install @st-one-io/nodes7@1.1.2
const S7ItemGroup = require('@st-one-io/nodes7/src/s7itemGroup.js');
const PDU = 960;
const fakeEndpoint = { pduSize: PDU, isConnected: false, on(){}, once(){}, removeListener(){} };
// 40 items of 21 bytes (odd length), spaced 42 bytes apart so they
// cannot be merged into a single contiguous part (gap > optimizationGap)
const vars = {};
for (let i = 0; i < 40; i++) vars['TAG' + i] = `DB1,B${i * 42}.21`;
const group = new S7ItemGroup(fakeEndpoint);
group.setTranslationCB(tag => vars[tag]);
group.addItems(Object.keys(vars));
group._prepareReadPackets();
group._readPackets.forEach((pkt, idx) => {
const req = 10 + 2 + 12 * pkt.length;
// actual S7 response size: header(12) + param(2) + per item: 4 + data,
// with data padded to even length for every item except the last
let res = 12 + 2;
pkt.forEach((part, j) => {
let dl = part.length;
if (j < pkt.length - 1 && dl % 2) dl += 1; // S7 fill byte
res += 4 + dl;
});
console.log(`packet ${idx}: parts=${pkt.length} reqLen=${req} actualResLen=${res}` +
(res > PDU ? ' <-- EXCEEDS PDU, PLC replies 0x8500' : ''));
});
Output with v1.1.2:
packet 0: parts=37 reqLen=456 actualResLen=975 <-- EXCEEDS PDU, PLC replies 0x8500
packet 1: parts=3 reqLen=48 actualResLen=91
Expected behavior
No prepared read packet should produce a response larger than the negotiated
PDU size. actualResLen must be <= 960 for every packet.
Real-world case
Production system: Node-RED with node-red-contrib-s7 3.1.3
(@st-one-io/nodes7 1.1.2), S7-1513-1 CPU, a variable table of ~200 entries
including 28 byte-array items of 21 bytes each (blocks of setpoints inside a
DB). The endpoint logged PLC error [0x8500]: S7 protocol error once per
polling cycle. Removing any one variable from the table made the error
disappear, adding it back made it return.
Suggested fix
In _prepareReadPackets(), account for the fill byte whenever a part with odd
length is added or extended, e.g. count partLength + (partLength % 2) in
pktResLength (the "last item is not padded" byte can be kept as slack).
The same consideration applies to the write path
(_prepareWritePackets), where odd-length payloads are padded in the request.
Workaround
Use even-length items only (e.g. we changed our 21-byte blocks to 22 bytes,
covering one padding byte of the UDT): with all parts even, no fill bytes
exist and the accounting is exact. Verified clean at PDU 240/480/960.
Environment
- @st-one-io/nodes7: 1.1.2 (via node-red-contrib-s7 3.1.3)
- Node.js: 18.x
- PLC: SIMATIC S7-1513-1 PN (negotiated PDU 960)
Description by ClaudeAI
This analysis comes from Claude Ai as a part of complex supervisor deployment
Describe the bug
S7ItemGroup._prepareReadPackets()builds read packets by summing the rawpart.lengthvalues againstmaxPayloadSize = pduSize - 18. However, in theS7 protocol every data item in a read response is padded to an even number
of bytes (fill byte), except the last one. The optimizer does not account for
these fill bytes.
When a packet contains many odd-length parts, the actual response can exceed
the negotiated PDU size even though the optimizer's own accounting says it
fits. The PLC then rejects the request with
PLC error [0x8500](L7 PDU sizeerror) on every read cycle, and the connection never recovers.
With
pduSize = 960the implicit safety margin is 18 bytes, so 19 or moreodd-length parts in the same packet are enough to overflow.
A nasty property of this bug: removing any single variable from the table
reshuffles the greedy bin-packing and usually makes the error disappear, which
makes it look random and very hard to diagnose in the field.
This may also be the root cause of #-related reports of
S7 protocol error: Wrong frameswith large variable tables (e.g.st-one-io/node-red-contrib-s7#134).
To Reproduce
Self-contained script, no PLC needed (only the packet preparation is
exercised):
Output with v1.1.2:
Expected behavior
No prepared read packet should produce a response larger than the negotiated
PDU size.
actualResLenmust be <= 960 for every packet.Real-world case
Production system: Node-RED with node-red-contrib-s7 3.1.3
(@st-one-io/nodes7 1.1.2), S7-1513-1 CPU, a variable table of ~200 entries
including 28 byte-array items of 21 bytes each (blocks of setpoints inside a
DB). The endpoint logged
PLC error [0x8500]: S7 protocol erroronce perpolling cycle. Removing any one variable from the table made the error
disappear, adding it back made it return.
Suggested fix
In
_prepareReadPackets(), account for the fill byte whenever a part with oddlength is added or extended, e.g. count
partLength + (partLength % 2)inpktResLength(the "last item is not padded" byte can be kept as slack).The same consideration applies to the write path
(
_prepareWritePackets), where odd-length payloads are padded in the request.Workaround
Use even-length items only (e.g. we changed our 21-byte blocks to 22 bytes,
covering one padding byte of the UDT): with all parts even, no fill bytes
exist and the accounting is exact. Verified clean at PDU 240/480/960.
Environment
Description by ClaudeAI
This analysis comes from Claude Ai as a part of complex supervisor deployment