# 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. ```bash 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: ```python 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.