Skip to content

Fix bit-device read point count for optimized blocks - #19

Open
parthi2929 wants to merge 1 commit into
plcpeople:masterfrom
parthi2929:fix/bit-read-point-count-optimized-blocks
Open

parthi2929 wants to merge 1 commit into
plcpeople:masterfrom
parthi2929:fix/bit-read-point-count-optimized-blocks

Conversation

@parthi2929

@parthi2929 parthi2929 commented Aug 13, 2026 •

Copy link
Copy Markdown

Problem

Reading more than one bit device (M, X, Y, …) fails whenever the read optimizer merges those items into a single block that spans more than one 16-bit word. Every item in the affected block comes back with bad quality (255) and the console fills with:

Invalid Response Length - Expected 6 but got 2 bytes.
Invalid Response Length - Expected 8 but got 2 bytes.

Single bit items, and groups that happen to fall inside one word, work fine — which makes this look intermittent and address-dependent rather than systematic.

Root cause

prepareReadPacket's optimizer merges nearby bit items into one block and grows that block's byteLength to cover the whole span:

https://github.com/plcpeople/mcprotocol/blob/master/mcprotocol.js#L631-L639

arrayLength and remainder, though, keep describing only the first item in the block.

Both address builders derived the SLMP number of device points field from Math.ceil((arrayLength + remainder) / 16):

  • MCAddrToBuffer1E — L1883
  • MCAddrToBuffer3E — L1989 (ASCII), L2003 (binary)

For a merged block that still evaluates to 1 word, so the request under-asks. The PLC answers correctly with the single word it was asked for, and then processMBPacket compares the reply against the block's byteLength and rejects it, failing every item in the block:

https://github.com/plcpeople/mcprotocol/blob/master/mcprotocol.js#L1269-L1304

The PLC is behaving correctly throughout; the request itself is malformed.

Minimal reproduction

Two bit tags, 21 bits apart, so they merge into one block spanning two words:

const net = require('net');
const MCP = require('./mcprotocol.js');

const server = net.createServer((sock) => {
  sock.on('data', (buf) => {
    const points = buf.readUInt8(10);
    console.log('PLC asked for %d point(s) at head device %d', points, buf.readUInt32LE(4));
    sock.write(Buffer.concat([Buffer.from([0x81, 0x00]), Buffer.alloc(points * 2, 0)]));
  });
});

server.listen(55123, '127.0.0.1', () => {
  const conn = new MCP();
  conn.initiateConnection({ host: '127.0.0.1', port: 55123, ascii: false }, () => {
    conn.setTranslationCB((t) => t);
    conn.addItems(['M0', 'M20']);
    conn.readAllItems((bad, values) => console.log(bad, values));
  });
});

The stand-in server answers with exactly the number of words the library asks for, as a real PLC does.

Before:

PLC asked for 1 point(s) at head device 0
Invalid Response Length - Expected 4 but got 2 bytes.
true { M0: 'BAD 255', M20: 'BAD 255' }

After:

PLC asked for 2 point(s) at head device 0
false { M0: false, M20: false }

The fix

Bit reads already use the word-unit subcommand, so the point count is simply byteLength / 2.

This is the same expression the non-bit branch of both functions uses, a few lines below each changed line — and the // doesn't work with optimized blocks where array length isn't right comment sitting immediately after each site suggests the word path was corrected for exactly this reason while the bit path was missed.

No behaviour change for unoptimized items: byteLength is set to wordLength * 2 in stringToMCAddr, so a lone bit item still requests one word. The optimizer also forces byteLength even, so byteLength / 2 is always an integer.

Writes are untouched — only the bitNative && !isWriting branches change.

Verified

Against master (f174d5b), with a stand-in PLC as above, and additionally on real hardware: a Mitsubishi FX5 over SLMP, where a project with 78 bit tags went from 3 readable to all 78 readable, with the Invalid Response Length messages and the resulting bad-quality reads gone entirely.

Reported downstream in FUXA, which pins mcprotocol@0.1.2 for its MELSEC driver: frangoteam/FUXA#2512

When several bit items (M/X/Y/etc) are close enough together, the read
optimizer merges them into a single block and grows the block's
byteLength to span the whole range. arrayLength and remainder, however,
continue to describe only the first item in the block.

Both MCAddrToBuffer1E and MCAddrToBuffer3E derived the SLMP "number of
device points" field from ceil((arrayLength + remainder) / 16), so a
merged block spanning several words still requested a single word. The
PLC replies correctly with the one word it was asked for, then
processMBPacket compares that against the block's byteLength and rejects
it with "Invalid Response Length - Expected N but got 2 bytes", marking
every item in the block bad quality.

Bit reads use word-unit subcommands, so byteLength/2 is the correct
point count. For an unoptimized item byteLength is already wordLength*2,
so single-item reads are unaffected; the optimizer also keeps byteLength
even. This mirrors what the non-bit branch of both functions already
does.

Minimal reproduction - two bit tags landing in different words:

  conn.addItems(['M0', 'M20']);
  conn.readAllItems(cb);

  before: head=M0 points=1 -> "Invalid Response Length - Expected 4 but
          got 2 bytes", M0 and M20 both BAD 255
  after:  head=M0 points=2 -> both items read, anythingBad === false

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant