feat: add NL-52 USB bulk probe implementation
fix: jinja2 dict hash bug
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RION NL-42/NL-52 USB bulk probe — talks to the meter via libusb directly
|
||||
(ctypes, no pyusb/pip needed).
|
||||
|
||||
The NL-52's USB serial interface (interface 0) is a plain vendor class with
|
||||
two bulk endpoints and NO control protocol — not FTDI/CDC, so no kernel
|
||||
serial driver applies. We just claim interface 0 and move ASCII over the
|
||||
bulk pipe: write command -> EP 0x03 OUT, read reply <- EP 0x84 IN.
|
||||
|
||||
Needs write access to the USB device node (root, or a udev rule granting the
|
||||
plugdev group). Run: sudo python3 nl52/usb_bulk_probe.py
|
||||
"""
|
||||
|
||||
import ctypes as C
|
||||
import time
|
||||
|
||||
VID, PID = 0x0EA3, 0x000F
|
||||
EP_OUT, EP_IN = 0x03, 0x84
|
||||
IFACE = 0
|
||||
|
||||
LIBUSB_ERROR_TIMEOUT = -7
|
||||
|
||||
lib = C.CDLL("libusb-1.0.so.0")
|
||||
lib.libusb_init.argtypes = [C.POINTER(C.c_void_p)]
|
||||
lib.libusb_open_device_with_vid_pid.argtypes = [C.c_void_p, C.c_uint16, C.c_uint16]
|
||||
lib.libusb_open_device_with_vid_pid.restype = C.c_void_p
|
||||
lib.libusb_set_auto_detach_kernel_driver.argtypes = [C.c_void_p, C.c_int]
|
||||
lib.libusb_claim_interface.argtypes = [C.c_void_p, C.c_int]
|
||||
lib.libusb_release_interface.argtypes = [C.c_void_p, C.c_int]
|
||||
lib.libusb_set_interface_alt_setting.argtypes = [C.c_void_p, C.c_int, C.c_int]
|
||||
lib.libusb_clear_halt.argtypes = [C.c_void_p, C.c_ubyte]
|
||||
lib.libusb_close.argtypes = [C.c_void_p]
|
||||
lib.libusb_exit.argtypes = [C.c_void_p]
|
||||
lib.libusb_bulk_transfer.argtypes = [
|
||||
C.c_void_p, C.c_ubyte, C.POINTER(C.c_ubyte), C.c_int, C.POINTER(C.c_int), C.c_uint
|
||||
]
|
||||
lib.libusb_strerror.argtypes = [C.c_int]
|
||||
lib.libusb_strerror.restype = C.c_char_p
|
||||
|
||||
|
||||
def err(code):
|
||||
return lib.libusb_strerror(code).decode(errors="replace")
|
||||
|
||||
|
||||
def main():
|
||||
ctx = C.c_void_p()
|
||||
if lib.libusb_init(C.byref(ctx)) != 0:
|
||||
print("[!] libusb_init failed")
|
||||
return 1
|
||||
|
||||
h = lib.libusb_open_device_with_vid_pid(ctx, VID, PID)
|
||||
if not h:
|
||||
print(f"[!] open {VID:04x}:{PID:04x} failed — device present? running as root?")
|
||||
lib.libusb_exit(ctx)
|
||||
return 1
|
||||
|
||||
lib.libusb_set_auto_detach_kernel_driver(h, 1)
|
||||
rc = lib.libusb_claim_interface(h, IFACE)
|
||||
if rc != 0:
|
||||
print(f"[!] claim_interface({IFACE}) failed: {err(rc)}")
|
||||
lib.libusb_close(h); lib.libusb_exit(ctx)
|
||||
return 1
|
||||
|
||||
print(f"[*] Claimed interface {IFACE} on {VID:04x}:{PID:04x} — bulk OUT 0x{EP_OUT:02x}, IN 0x{EP_IN:02x}")
|
||||
|
||||
# Select alt setting 0 explicitly and clear any endpoint halts left over
|
||||
# from prior (wrong-driver) probing.
|
||||
rc = lib.libusb_set_interface_alt_setting(h, IFACE, 0)
|
||||
print(f"[*] set_interface_alt_setting(0): {err(rc) if rc else 'ok'}")
|
||||
for ep in (EP_OUT, EP_IN):
|
||||
rc = lib.libusb_clear_halt(h, ep)
|
||||
print(f"[*] clear_halt(0x{ep:02x}): {err(rc) if rc else 'ok'}")
|
||||
print()
|
||||
|
||||
def bulk_out(data: bytes, timeout=2000):
|
||||
buf = (C.c_ubyte * len(data)).from_buffer_copy(data)
|
||||
actual = C.c_int(0)
|
||||
rc = lib.libusb_bulk_transfer(h, EP_OUT, buf, len(data), C.byref(actual), timeout)
|
||||
return rc, actual.value
|
||||
|
||||
def bulk_in(n=512, timeout=2000):
|
||||
buf = (C.c_ubyte * n)()
|
||||
actual = C.c_int(0)
|
||||
rc = lib.libusb_bulk_transfer(h, EP_IN, buf, n, C.byref(actual), timeout)
|
||||
return rc, bytes(buf[:actual.value])
|
||||
|
||||
def exchange(cmd: str):
|
||||
rc, n = bulk_out((cmd + "\r\n").encode("ascii"))
|
||||
if rc != 0:
|
||||
print(f" > {cmd!r:24} OUT failed: {err(rc)}")
|
||||
return
|
||||
# Read until idle (collect result code + data lines)
|
||||
chunks = bytearray()
|
||||
end = time.time() + 2.0
|
||||
while time.time() < end:
|
||||
rrc, data = bulk_in(512, 600)
|
||||
if rrc == 0 and data:
|
||||
chunks.extend(data)
|
||||
end = min(end, time.time() + 0.4) # brief idle window then stop
|
||||
elif rrc == LIBUSB_ERROR_TIMEOUT:
|
||||
if chunks:
|
||||
break
|
||||
else:
|
||||
if rrc != 0:
|
||||
break
|
||||
print(f" > {cmd!r:24} -> {bytes(chunks)!r}")
|
||||
|
||||
# Read-first sanity check: is the IN endpoint alive / does the device send
|
||||
# anything unsolicited?
|
||||
rrc, data = bulk_in(512, 1000)
|
||||
print(f"[*] read-first IN: rc={err(rrc) if rrc else 'ok'} data={data!r}\n")
|
||||
|
||||
try:
|
||||
for cmd in ["System Version?NL", "Measure?", "DOD?", "Battery Type?"]:
|
||||
exchange(cmd)
|
||||
time.sleep(0.3)
|
||||
finally:
|
||||
lib.libusb_release_interface(h, IFACE)
|
||||
lib.libusb_close(h)
|
||||
lib.libusb_exit(ctx)
|
||||
|
||||
print("\n[done]")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user