read.py 4.7 KB

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