Skip to content

Read packet optimizer does not account for S7 fill bytes on odd-length parts -> PLC error [0x8500] (PDU size exceeded) #17

Description

@marcoguerra171

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions