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