quirks.md 4.5 KB

MUST PV1800 quirks

Things that bit me while writing must_pv1800_monitor.py. Read these before debugging "why isn't it working".

1. The firmware ignores your Modbus count

Ask for register 15201 with count=1 and the PV1800 will happily dump 49 registers (98 bytes of data + 5 bytes overhead = 103 bytes per response). Sometimes it dumps 39 registers (bc=0x4e) instead. There doesn't seem to be a pattern — same request can produce different sizes on different runs.

Consequence: Modbus RTU response frame has no address field, so when multiple frames get queued in the transport you can't tell which one is yours.

Fix in this code: read the entire block (15201..15208 for CHARGER, 25201..25274 for INVERTER, 20118..20132 for SETTINGS) in ONE request. The response unambiguously starts at the address you asked for, and the registers you don't care about get thrown away. See poll_block() in the script.

2. Don't use socat to bridge the serial port

I spent an embarrassing amount of time on this one.

  • cat /dev/ttyUSB0 exits on EOF and the SSH channel collapses.
  • socat ... rawer mode WORKS only when the kernel TTY buffer has stale data in it. Once the buffer is clean, socat can't wake the CH340 driver up reliably. Bytes get stuck in the kernel buffer and never reach the inverter.
  • Don't add waitlock=N to socat. It kills the read path.

What works: a Python proxy using pyserial. pyserial calls TIOCEXCL on open, which is what the PV1800's RS485 driver actually waits for. See _REMOTE_PROXY_SCRIPT and _SSHTunneledSerial in the script.

3. Pre-flush the kernel TTY buffer at proxy startup

Stale bytes from previous runs (or from a half-killed dd, cat, or socat) sit in the kernel TTY buffer. The very first read after the proxy starts will pick them up and corrupt its response.

Fix in this code: the embedded proxy opens a temporary serial.Serial(...) with a 200 ms timeout, then read-and-discards until the line has been quiet for 0.3 s (or 3 s total). This empties the kernel buffer before the real bridge opens.

4. Zombies love your TTY

Zombie bash processes doing dd if=/dev/ttyUSB0 or cat /dev/ttyUSB0 will eat every Modbus response silently. fuser -k /dev/ttyUSB0 is not enough — it only kills processes that currently have the port open. You also have to pkill by name.

pkill -9 -f 'cat /dev/ttyUSB0'
pkill -9 -f 'dd if=/dev/ttyUSB0'
pkill -9 -f 'socat.*ttyUSB0'

(There may be a SCREEN /dev/ttyUSB1 session from another admin on your system — leave that alone, it's on the FTDI adapter, not the CH340 we're using.)

5. paramiko recv_exit_status() before recv() is a trap

recv_exit_status() waits for the remote command to exit, and in doing so it can swallow the command's stdout. The recommended pattern is:

out = b""
try:
    while True:
        chunk = ch.recv(4096)
        if not chunk:
            break
        out += chunk
except Exception:
    pass
try:
    ch.recv_exit_status()
except Exception:
    pass

Read ALL bytes first, THEN call recv_exit_status. See _SSHTunneledSerial.__init__ for the working pattern.

6. 250 ms inter-block delay is the sweet spot

Polling three register blocks back-to-back (CHARGER + INVERTER + SETTINGS) leaves the inverter's UART ring buffer with stale data that's still trickling in. Faster than 250 ms and your next read sees stale bytes; slower than that and a full snapshot takes ~10 s (3 blocks × ~3 s each at 19200 baud).

7. The Modbus spec says responses don't echo the address

The PV1800 follows the spec — its response is [slave func bc data... CRC]. So when the inverter dumps a 39-register block and my parser is sitting on a multi-frame buffer, I have no way to ask "is this my frame?" The CRC check is the only ground truth: a frame whose CRC validates is by definition a complete, uncorrupted Modbus RTU response — maybe not the one I want, but a real one.

In practice this means: always read whole blocks at once. If you ask for a single register and the inverter responds with 39, you've got 38 extra registers of data with no way to know what address they belong to (only that the block starts somewhere in 15201..15249).

8. Modbus RTU inter-frame silence is ~3.5 character times

At 19200 8N1, that's about 1.8 ms. The inverter respects this. Most timing issues we've seen are NOT inter-frame but rather intra-frame — the inverter pauses in the middle of a long dump for ~10 ms every so often. Long enough to make my 1 s timeout fire, but I usually catch the full frame on the second try.