must_pv1800_proxy.py 2.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. #!/usr/bin/env python3
  2. """Tiny TCP<->serial bridge for MUST PV1800 monitoring.
  3. Listens on 127.0.0.1:$PROXY_PORT, forwards every byte to/from $PROXY_TTY
  4. using pyserial. Multiple sequential TCP clients are served one at a time.
  5. """
  6. import os, sys, threading, time
  7. import serial, socket
  8. PORT = int(os.environ.get("PROXY_PORT", "9700"))
  9. TTY = os.environ.get("PROXY_TTY", "/dev/ttyUSB0")
  10. BAUD = int(os.environ.get("PROXY_BAUD", "19200"))
  11. def log(msg):
  12. print(f"[must-proxy] {msg}", file=sys.stderr, flush=True)
  13. try:
  14. # Pre-flush: read-and-discard whatever stale bytes are sitting in the
  15. # kernel TTY buffer from previous runs. Without this, those bytes get
  16. # delivered to the first client and corrupt its first read.
  17. pre = serial.Serial(TTY, BAUD, timeout=0.2)
  18. pre.reset_input_buffer()
  19. pre.reset_output_buffer()
  20. flushed = 0
  21. quiet_for = 0.0
  22. pre_start = time.monotonic()
  23. while time.monotonic() - pre_start < 3.0 and quiet_for < 0.3:
  24. chunk = pre.read(512)
  25. if chunk:
  26. flushed += len(chunk)
  27. quiet_for = 0.0
  28. else:
  29. quiet_for += 0.05
  30. time.sleep(0.05)
  31. pre.close()
  32. if flushed:
  33. log(f"flushed {flushed} stale bytes from kernel TTY buffer")
  34. ser = serial.Serial(TTY, BAUD, timeout=0.05)
  35. ser.reset_input_buffer()
  36. ser.reset_output_buffer()
  37. log(f"opened {TTY} @ {BAUD} fd={ser.fd}")
  38. except Exception as e:
  39. log(f"could not open {TTY}: {e}")
  40. sys.exit(1)
  41. def tty_to_tcp(client):
  42. while True:
  43. try:
  44. data = ser.read(256)
  45. if data:
  46. client.sendall(data)
  47. except Exception:
  48. return
  49. srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  50. srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  51. srv.bind(("127.0.0.1", PORT))
  52. srv.listen(5)
  53. log(f"listening on 127.0.0.1:{PORT}")
  54. while True:
  55. client, addr = srv.accept()
  56. log(f"client {addr}")
  57. threading.Thread(target=tty_to_tcp, args=(client,), daemon=True).start()
  58. try:
  59. while True:
  60. data = client.recv(4096)
  61. if not data:
  62. break
  63. ser.write(data)
  64. ser.flush()
  65. except Exception as e:
  66. log(f"tcp err: {e}")
  67. try:
  68. client.close()
  69. except Exception:
  70. pass
  71. log(f"client closed")