Skip to content

Fix XcomRS232: frame responses by scom length field (not CR/LF) and open the port as 8E1 - #9

Open
njalooo wants to merge 1 commit into
zocker-160:masterfrom
njalooo:master
Open

Fix XcomRS232: frame responses by scom length field (not CR/LF) and open the port as 8E1#9
njalooo wants to merge 1 commit into
zocker-160:masterfrom
njalooo:master

Conversation

@njalooo

@njalooo njalooo commented Jun 24, 2026

Copy link
Copy Markdown

Summary

XcomRS232 cannot communicate with a bare Xcom-232i over a direct RS-232
link. Every request times out with AssertionError: got empty response,
100% of the time. This PR fixes two related defects in XcomRS232.sendPackage:

  1. Response framing. The implementation read the response with
    ser.read_until(SERIAL_TERMINATOR) where SERIAL_TERMINATOR = b'\x0D\x0A'.
    A bare Xcom-232i does not append CR/LF to its replies — the scom
    protocol is self-delimited by the data_length field in the frame header.
    read_until(CRLF) therefore waits for a delimiter that never arrives,
    times out, and returns nothing (and the subsequent response[:-2] slice
    would corrupt the frame even when bytes did arrive).

  2. Serial parity. The port was opened with
    serial.Serial(device, baudrate, timeout=...), i.e. pyserial's default
    8N1. The Xcom-232i requires 8E1 (EVEN parity). With 8N1 the 232i
    silently rejects every frame (the scom data-link layer drops frames with a
    parity/checksum error and sends no response — see the Studer "Technical
    specification - Xtender serial protocol", data-link layer error handling).

Both defects independently produce the same symptom (empty response), which
is why direct-serial use of the library failed completely.

Why CR/LF was wrong here

The CR/LF terminator is not a property of the Xcom-232i. It is an artifact
of the Moxa NPort "Data Packing" feature used in an Xcom-LAN setup, where
a Moxa gateway bridges the 232i's serial port to TCP and is configured to
delimit/pack TCP segments on 0x0D 0x0A. That path is handled separately by
the XcomLANTCP / XcomLANUDP classes over the network — and notably, even
those classes do not use read_until(CRLF); they recv() and call
Package.parseBytes() directly. So the CR/LF terminator was present only in
the serial class, where it never applies. A Moxa is never in front of the
XcomRS232 serial transport (if a Moxa is present, you reach it via the LAN
classes over Ethernet). The CR/LF handling in XcomRS232 was therefore simply
misplaced.

The fix

XcomRS232.sendPackage now:

  • opens the port explicitly as 8E1 (bytesize=EIGHTBITS,
    parity=PARITY_EVEN, stopbits=STOPBITS_ONE); parity is also exposed as a
    constructor argument defaulting to EVEN, for the rare case of a 232i
    reconfigured otherwise;
  • reads the response by letting Package.parse(ser) consume exactly one
    self-delimited scom frame directly from the serial stream — it seeks the
    0xAA start byte, reads the 11-byte header + 2 header-checksum bytes, then
    data_length body bytes + 2 data-checksum bytes, validating both checksums.
    No CR/LF is appended to the request or expected in the response.

This reuses the existing, already-correct Package.parse() streaming parser,
so no protocol logic is duplicated.

Diagnosis (how this was isolated)

The root cause was found by elimination against a known-good reference:

  • An independent, hand-rolled scom scanner (raw pyserial, length-based read,
    38400 8E1) reliably read info object 3000 (battery voltage) and other
    values from XT1/XT2/XT3 (addresses 101–103), the BSP (601), and the gateway
    alias (1) — proving the wiring, baud, parity, addressing, frame encoding and
    checksum were all correct, and the 232i was healthy.
  • The library, on the same port at the same time, returned empty for the
    exact same object 3000. This localised the fault to the library's request/
    response handling, not the link.
  • Source address was ruled out: the scanner worked with both src=1 and
    src=1000, matching the library's src=1.
  • Reading XcomRS232.py revealed read_until(SERIAL_TERMINATOR) and the
    absence of any parity argument on serial.Serial(...). Cross-checking
    protocol.py (Package.parse / Header.length = 11) and XcomLAN.py
    (which does not use CR/LF) confirmed the CR/LF was misplaced and the
    parser already supported length-based framing.

Testing

Hardware: ASUS Tinker Board (Armbian, Debian 13 trixie, armhf) → StarTech
USB-RS232 adapter (Prolific PL2303GC) → straight-through DE-9 → Studer
Xcom-232i → Studer bus with 3× Xtender XTM 4000-48 (addr 101–103) and a BSP
(addr 601). Serial link: 38400 8E1, no flow control.

Before the fix: every getValue / getValueByID raised
AssertionError: got empty response.

After the fix, on real hardware:

XT1 uBat    (info 3000)      = 50.625 V
1140 Floating voltage        = 51.0146 V
1156 Absorption voltage      = 52.0224 V
1155 Absorption allowed      = True
1163 Equalization allowed    = False
1170 Reduced floating allowed= False
1173 Periodic absorp allowed = False
1108 Undervoltage no-load    = 44.0303 V
1109 Undervoltage full-load  = 42.0 V
1138 Battery charge current  = 30.0 A
1125 Charger allowed         = True

All reads return correct, sensible values, repeatably, against XT1 (101) and
the BSP (601). Parameter reads (TYPE_PARAMETER, 1xxx IDs) and info reads
(TYPE_INFO, 3000) both work.

Note on baud rate: the library examples use 115200; the Xcom-232i default is
38400. The fix is baud-independent — these tests used 38400 because that is
the device default — but worth noting in case users hit an empty response that
is actually a baud mismatch.

Backwards compatibility

  • Public API unchanged for existing callers: XcomRS232(serialDevice=..., baudrate=...) now additionally defaults to 8E1, which is required by the
    hardware. A new optional parity= argument is added (default PARITY_EVEN).
  • The Moxa / Xcom-LAN path (XcomLANTCP, XcomLANUDP) is untouched.
  • SERIAL_TERMINATOR / MSG_MAX_LENGTH import usage is removed from the
    serial read path.

Risk

Low. The change is confined to XcomRS232.sendPackage and its constructor,
reuses the existing Package.parse() parser, and does not affect the LAN
transports or the protocol layer.

… as 8E1

The Xcom-232i over raw RS-232 does not append the CR/LF terminator used by
Xcom-LAN/Moxa Data Packing, and scom frames are self-delimiting by the
header data_length field. read_until(CRLF) therefore timed out (empty
response) on a bare 232i. Also open the port with EVEN parity (8E1) as
required, instead of relying on pyserial's default 8N1.
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