read.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. #!/usr/bin/env python3
  2. """
  3. This module connects to serial port and exposes the results in stdout.
  4. """
  5. import decoder
  6. import json
  7. from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
  8. from typing import Optional
  9. import atexit
  10. import time
  11. import logging
  12. import sys
  13. import multiprocessing
  14. import serial
  15. import requests
  16. log = logging.getLogger("seriow")
  17. def read_auth(buf) -> tuple[str, str]:
  18. username = buf.readline()
  19. password = buf.readline()
  20. buf.close()
  21. return (username.rstrip(), password.rstrip("\n"))
  22. class MessageForwarder:
  23. def __init__(self):
  24. self.base_url: Optional[str] = None
  25. self.auth: Optional[tuple[str, str]] = None
  26. def initialize_from_args(self, args):
  27. pass
  28. @property
  29. def request_params(self) -> dict:
  30. r = {}
  31. if self.auth is not None:
  32. r["auth"] = self.auth
  33. return r
  34. def send_log(self, message: decoder.LogMessage):
  35. requests.post(
  36. f"{self.base_url}/messages", json=message.asdict(), **self.request_params
  37. )
  38. def send_dump(self, message: decoder.DumpMessage):
  39. requests.post(
  40. f"{self.base_url}/variables", json=message.asdict(), **self.request_params
  41. )
  42. def send_message(self, message: decoder.Message):
  43. if not self.base_url:
  44. return
  45. if isinstance(message, decoder.LogMessage):
  46. self.send_log(message)
  47. elif isinstance(message, decoder.DumpMessage):
  48. self.send_dump(message)
  49. def get_next_message(serial) -> Optional[bytes]:
  50. """
  51. >>> from io import BytesIO
  52. >>> msg = 'foo\\x01LOG D ciao\\n'
  53. >>> get_next_message(BytesIO(msg.encode('ascii'))).rstrip().decode('ascii')
  54. 'LOG D ciao'
  55. >>> msg = 'foo\\nasd\\x01LOG D ciao\\n'
  56. >>> get_next_message(BytesIO(msg.encode('ascii'))).rstrip().decode('ascii')
  57. 'LOG D ciao'
  58. """
  59. while True:
  60. c = serial.read(1)
  61. log.info("%r", c)
  62. if not c:
  63. return None
  64. if ord(c) == 1:
  65. break
  66. return serial.readline() # read a '\n' terminated line
  67. def loop(serial, forwarder: MessageForwarder, args):
  68. dec = decoder.Decoder()
  69. while True:
  70. line = get_next_message(serial)
  71. try:
  72. message = dec.decode(line)
  73. except Exception:
  74. continue
  75. if message is None:
  76. continue
  77. obj = (str(type(message)), message.asdict())
  78. print(json.dumps(obj))
  79. multiprocessing.Process(target=forwarder.send_message, args=(message,)).start()
  80. def close_all(serial):
  81. serial.close()
  82. def get_parser() -> ArgumentParser:
  83. parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
  84. serial_parser = parser.add_argument_group("serial options")
  85. serial_parser.add_argument(
  86. "--device",
  87. default="hwgrep://ttyUSB",
  88. help="Device path, or URL as in https://pyserial_parser.readthedocs.io/en/latest/url_handlers.html",
  89. )
  90. serial_parser.add_argument("--baudrate", type=int, default=115200)
  91. serial_parser.add_argument(
  92. "--wait",
  93. action="store_true",
  94. default=False,
  95. help="Wait until serial is found, and retries upon failures",
  96. )
  97. http_parser = parser.add_argument_group("http options")
  98. http_parser.add_argument(
  99. "--http-endpoint", metavar="URL", help="sth like http://127.0.0.1:8000/"
  100. )
  101. http_parser.add_argument(
  102. "--http-auth-file",
  103. type=open,
  104. metavar="FILE",
  105. help="Path to a file with two lines: first is username, second is password",
  106. )
  107. parser.add_argument("--verbose", "-v", action="store_true", default=False)
  108. return parser
  109. def main():
  110. args = get_parser().parse_args()
  111. logging.basicConfig(level=logging.INFO)
  112. log.setLevel(logging.INFO if args.verbose else logging.WARN)
  113. log.info("Connecting...")
  114. forwarder = MessageForwarder()
  115. if args.http_endpoint:
  116. forwarder.base_url = args.http_endpoint
  117. if args.http_auth_file is not None:
  118. forwarder.auth = read_auth(args.http_auth_file)
  119. while True:
  120. try:
  121. s = serial.serial_for_url(args.device, do_not_open=True)
  122. s.baudrate = args.baudrate
  123. s.open()
  124. except Exception as exc:
  125. if not args.wait:
  126. log.info("Cannot connect: %s", exc)
  127. sys.exit(1)
  128. log.info("Cannot connect, will retry...")
  129. time.sleep(1)
  130. continue
  131. log.info("Connected!")
  132. atexit.register(close_all, s)
  133. try:
  134. loop(s, forwarder, args)
  135. except serial.serialutil.SerialException:
  136. if not args.wait:
  137. sys.exit(1)
  138. if __name__ == "__main__":
  139. main()