Things that bit me while writing must_pv1800_monitor.py. Read these
before debugging "why isn't it working".
countAsk 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.
socat to bridge the serial portI 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.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.
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.
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.)
recv_exit_status() before recv() is a traprecv_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.
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).
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).
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.