quick nl52 usb probe
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RION NL-42 / NL-52 USB serial probe — zero dependencies (stdlib termios only).
|
||||
|
||||
The NL-52's USB port enumerates as a virtual COM port ("RION USB to RS232C
|
||||
Converter"). On Linux that is almost always handled by an in-kernel USB-serial
|
||||
driver (ftdi_sio / cp210x / ch341) which creates /dev/ttyUSB*. This script
|
||||
opens that port and sends a few harmless REQUEST commands (no settings are
|
||||
changed) to confirm two-way communication.
|
||||
|
||||
Protocol (NL-42/NL-52 Serial Interface Manual 55779):
|
||||
Request: "<Command>?" + CRLF
|
||||
Setting: "$<Command>,<param>" + CRLF (NOT used here — read-only probe)
|
||||
Reply: result code "R+0000" + CRLF, then data line(s) for requests.
|
||||
|
||||
Before running:
|
||||
1. On the meter: MENU -> I/O -> Communication Interface -> "USB"
|
||||
(set this BEFORE plugging in the cable).
|
||||
2. Connect a generic USB-A -> mini-B cable directly (no hub).
|
||||
3. Find the port: ls -l /dev/ttyUSB* (and `dmesg | tail` after plugging in)
|
||||
|
||||
Usage:
|
||||
python3 nl52_usb_probe.py # defaults to /dev/ttyUSB0
|
||||
python3 nl52_usb_probe.py --port /dev/ttyUSB0 --baud 115200
|
||||
python3 nl52_usb_probe.py --cmd "System Version?" --cmd "Clock?"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import select
|
||||
import sys
|
||||
import termios
|
||||
import time
|
||||
|
||||
# Safe, read-only probe commands (all are pure requests).
|
||||
DEFAULT_COMMANDS = [
|
||||
"System Version?", # firmware version — proves the link end-to-end
|
||||
"Clock?", # current date/time
|
||||
"SD Card Free Size?",
|
||||
"DOD?", # snapshot of currently displayed values
|
||||
]
|
||||
|
||||
BAUD_CONSTANTS = {
|
||||
9600: termios.B9600,
|
||||
19200: termios.B19200,
|
||||
38400: termios.B38400,
|
||||
57600: termios.B57600,
|
||||
115200: termios.B115200,
|
||||
}
|
||||
|
||||
|
||||
def open_serial(port: str, baud: int) -> int:
|
||||
"""Open a serial port in raw 8N1, no flow control. Returns an fd."""
|
||||
if baud not in BAUD_CONSTANTS:
|
||||
raise ValueError(f"Unsupported baud {baud}; choose from {sorted(BAUD_CONSTANTS)}")
|
||||
|
||||
fd = os.open(port, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
|
||||
|
||||
attrs = termios.tcgetattr(fd)
|
||||
iflag, oflag, cflag, lflag, ispeed, ospeed, cc = attrs
|
||||
|
||||
# Raw mode
|
||||
iflag = 0
|
||||
oflag = 0
|
||||
lflag = 0
|
||||
# 8 data bits, enable receiver, ignore modem control lines
|
||||
cflag = termios.CS8 | termios.CREAD | termios.CLOCAL
|
||||
# (no PARENB = no parity, no CSTOPB = 1 stop bit, no CRTSCTS = no flow control)
|
||||
|
||||
bconst = BAUD_CONSTANTS[baud]
|
||||
ispeed = bconst
|
||||
ospeed = bconst
|
||||
|
||||
termios.tcsetattr(fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc])
|
||||
termios.tcflush(fd, termios.TCIOFLUSH)
|
||||
return fd
|
||||
|
||||
|
||||
def send(fd: int, line: str):
|
||||
os.write(fd, (line + "\r\n").encode("ascii"))
|
||||
|
||||
|
||||
def read_reply(fd: int, timeout: float = 3.0) -> bytes:
|
||||
"""Read whatever arrives within `timeout` seconds (idle-gap terminated)."""
|
||||
buf = bytearray()
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
r, _, _ = select.select([fd], [], [], 0.3)
|
||||
if r:
|
||||
try:
|
||||
chunk = os.read(fd, 4096)
|
||||
except BlockingIOError:
|
||||
continue
|
||||
if chunk:
|
||||
buf.extend(chunk)
|
||||
# Once we've seen a CRLF and there's a brief idle, stop early
|
||||
deadline = min(deadline, time.time() + 0.4)
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="RION NL-42/NL-52 USB serial probe")
|
||||
ap.add_argument("--port", default="/dev/ttyUSB0")
|
||||
ap.add_argument("--baud", type=int, default=115200,
|
||||
help="USB CDC usually ignores baud, but RS-232C needs a match")
|
||||
ap.add_argument("--cmd", action="append", dest="cmds",
|
||||
help="Override probe command(s); repeatable")
|
||||
ap.add_argument("--timeout", type=float, default=3.0)
|
||||
args = ap.parse_args()
|
||||
|
||||
commands = args.cmds or DEFAULT_COMMANDS
|
||||
|
||||
if not os.path.exists(args.port):
|
||||
print(f"[!] {args.port} does not exist.")
|
||||
print(" Plug in the meter (Comm Interface = USB) and check: ls -l /dev/ttyUSB*")
|
||||
print(" Also check the kernel saw it: dmesg | tail -20")
|
||||
return 2
|
||||
|
||||
try:
|
||||
fd = open_serial(args.port, args.baud)
|
||||
except PermissionError:
|
||||
print(f"[!] Permission denied on {args.port}.")
|
||||
print(" Add yourself to the 'dialout' group, or run with sudo:")
|
||||
print(f" sudo usermod -aG dialout $USER (then log out/in)")
|
||||
return 2
|
||||
except Exception as e:
|
||||
print(f"[!] Could not open {args.port}: {e}")
|
||||
return 2
|
||||
|
||||
print(f"[*] Opened {args.port} @ {args.baud} 8N1 (raw, no flow control)")
|
||||
print(f"[*] Sending {len(commands)} read-only request command(s)\n")
|
||||
|
||||
ok = 0
|
||||
try:
|
||||
for cmd in commands:
|
||||
send(fd, cmd)
|
||||
reply = read_reply(fd, args.timeout)
|
||||
decoded = reply.decode("ascii", errors="replace").replace("\r", "\\r").replace("\n", "\\n\n")
|
||||
if reply:
|
||||
ok += 1
|
||||
print(f" > {cmd}")
|
||||
for line in decoded.splitlines():
|
||||
print(f" {line}")
|
||||
else:
|
||||
print(f" > {cmd}")
|
||||
print(f" (no response within {args.timeout}s)")
|
||||
print()
|
||||
time.sleep(1.0) # NL-series likes >=1s between commands
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
if ok == 0:
|
||||
print("[!] No responses. Things to check:")
|
||||
print(" - Meter's Communication Interface is set to USB (not RS-232C)")
|
||||
print(" - ECO / Sleep mode is OFF (both disable the comm interface)")
|
||||
print(" - Right port (try other /dev/ttyUSB* or /dev/ttyACM*)")
|
||||
print(" - For RS-232C path, baud must match the meter's setting")
|
||||
return 1
|
||||
|
||||
print(f"[OK] {ok}/{len(commands)} commands answered — two-way comms confirmed.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user