From 34f19ac539fa0ddcfea9389b8ded602e22a1170f Mon Sep 17 00:00:00 2001 From: Krisjanis Veinbahs Date: Sat, 8 Jan 2022 17:10:12 +0200 Subject: [PATCH] Allow custom monitoring scripts --- client/cli.py | 6 +- client/monitor_serial.py | 62 ++++++- client/monitor_serial_hexbytes.py | 50 +----- examples/monitor_serial_hexbytes/Pipfile | 12 ++ examples/monitor_serial_hexbytes/Pipfile.lock | 132 +++++++++++++++ examples/monitor_serial_hexbytes/mock.py | 114 +++++++++++++ examples/monitor_serial_hexbytes/monitor.py | 158 ++++++++++++++++++ 7 files changed, 483 insertions(+), 51 deletions(-) create mode 100644 examples/monitor_serial_hexbytes/Pipfile create mode 100644 examples/monitor_serial_hexbytes/Pipfile.lock create mode 100644 examples/monitor_serial_hexbytes/mock.py create mode 100644 examples/monitor_serial_hexbytes/monitor.py diff --git a/client/cli.py b/client/cli.py index c853e21..4e66143 100755 --- a/client/cli.py +++ b/client/cli.py @@ -11,7 +11,7 @@ from typing import List, Optional, Type from result import Err import backend_domain -from monitor_serial import MonitorSerial +from monitor_serial import MonitorSerial, MonitorSerialHelper from rich import print as richprint from rich_util import print_error, print_success, print_json from agent import AgentConfig @@ -542,7 +542,9 @@ def hardware_serial_monitor( sys.exit(1) # Monitor hardware - monitor_result = asyncio.run(backend_config.hardware_serial_monitor(hardware_id, monitor_class())) + monitor_result = asyncio.run(backend_config.hardware_serial_monitor( + hardware_id, + monitor_class(MonitorSerialHelper))) if isinstance(monitor_result, Err): print() print_error(f"Failed to monitor hardware: {monitor_result.value}") diff --git a/client/monitor_serial.py b/client/monitor_serial.py index 278c6b4..4f4776d 100644 --- a/client/monitor_serial.py +++ b/client/monitor_serial.py @@ -1,12 +1,72 @@ """Module for functionality related to serial socket monitoring""" from typing import Any -from protocol import MonitorListenerIncomingMessage, MonitorListenerOutgoingMessage +from protocol import \ + MonitorListenerIncomingMessage, \ + MonitorListenerOutgoingMessage, \ + MonitorUnavailable, \ + SerialMonitorMessageToClient, \ + SerialMonitorMessageToAgent from ws import Socketlike +from codec import CodecParseException + + +class MonitorSerialHelperLike: + """Helper for managing monitor messages""" + + @staticmethod + def isMonitorUnavailable(message: Any) -> bool: + """Check if message is of type 'MonitorUnavailable'""" + pass + + @staticmethod + def isSerialMonitorMessageToClient(message: Any) -> bool: + """Check if message is of type 'SerialMonitorMessageToClient'""" + pass + + @staticmethod + def isCodecParseException(instance: Any) -> bool: + """Check if class instance is of type 'CodecParseException'""" + pass + + @staticmethod + def createSerialMonitorMessageToAgent(payload: bytes) -> Any: + """Create createSerialMonitorMessageToAgent from bytes""" + pass + + +class MonitorSerialHelper(MonitorSerialHelperLike): + """Helper for managing monitor messages""" + + @staticmethod + def isMonitorUnavailable(message: Any) -> bool: + """Check if message is of type 'MonitorUnavailable'""" + return isinstance(message, MonitorUnavailable) + + @staticmethod + def isSerialMonitorMessageToClient(message: Any) -> bool: + """Check if message is of type 'SerialMonitorMessageToClient'""" + return isinstance(message, SerialMonitorMessageToClient) + + @staticmethod + def isCodecParseException(instance: Any) -> bool: + """Check if class instance is of type 'CodecParseException'""" + return isinstance(instance, CodecParseException) + + @staticmethod + def createSerialMonitorMessageToAgent(payload: bytes) -> Any: + """Create createSerialMonitorMessageToAgent from bytes""" + return SerialMonitorMessageToAgent.from_bytes(payload) class MonitorSerial: """Interface for serial socket monitors""" + + helper: MonitorSerialHelperLike + + def __init__(self, helper): + self.helper = helper + async def run( self, socketlike: Socketlike[Any, MonitorListenerIncomingMessage, MonitorListenerOutgoingMessage] diff --git a/client/monitor_serial_hexbytes.py b/client/monitor_serial_hexbytes.py index 4d473e7..f554458 100644 --- a/client/monitor_serial_hexbytes.py +++ b/client/monitor_serial_hexbytes.py @@ -25,52 +25,6 @@ LOGGER = log.timed_named_logger("monitor_serial") -# Mocks for DIP client classes for python type hinting -# N.B This whole section can be removed if you don't care about type checking -# and if you're just editing in Notepad and doing it trial-and-error style - -# class Socketlike(Generic[SERIALIZABLE, PI, PO]): -# """Interface for interactions w/ sockets""" -# async def connect(self) -> Optional[Exception]: -# pass -# -# async def disconnect(self) -> Optional[Exception]: -# pass -# -# async def rx(self) -> Result[PI, Exception]: -# pass -# -# async def tx(self, data: PO) -> Optional[Exception]: -# pass -# -# @dataclass(frozen=True, eq=False) -# class MonitorUnavailable: -# """Message regarding hardware monitor unavailability""" -# reason: str -# -# @dataclass(frozen=True, eq=False) -# class SerialMonitorMessageToClient: -# """Message from hardware serial monitor to client""" -# base64Bytes: str -# -# @staticmethod -# def from_bytes(content: bytes): -# """Construct message from bytes""" -# return SerialMonitorMessageToClient(base64.b64encode(content).decode("utf-8")) -# -# def to_bytes(self) -> bytes: -# """Construct bytes from message""" -# return base64.b64decode(self.base64Bytes) -# -# class MonitorSerial: -# """""" -# async def run( -# self, -# socketike: Socketlike[Any, protocol.MonitorListenerIncomingMessage, protocol.MonitorListenerOutgoingMessage] -# ): -# """Receive serial monitor websocket messages & implement user interfacing""" -# pass - # Actual monitor implementation class MonitorSerialHexbytes(MonitorSerial): @@ -173,7 +127,7 @@ async def run( # Redirect stdin to serial monitor socket asyncio_loop = asyncio.get_event_loop() stdin_capture_task = asyncio_loop.create_task( - MonitorSerialHexbytes.keep_transmitting_to_agent(socketlike)) + self.keep_transmitting_to_agent(socketlike)) # Define end-of-monitor handler death = Death() @@ -191,7 +145,7 @@ async def run( # Run monitoring loop while True: incoming_message_result = await socketlike.rx() - result = MonitorSerialHexbytes.render_message_data_or_finish(death, handle_finish, incoming_message_result) + result = self.render_message_data_or_finish(death, handle_finish, incoming_message_result) if result is not None: return result diff --git a/examples/monitor_serial_hexbytes/Pipfile b/examples/monitor_serial_hexbytes/Pipfile new file mode 100644 index 0000000..4b9c37a --- /dev/null +++ b/examples/monitor_serial_hexbytes/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +websockets = "*" +mypy = "*" +result = "*" + +[requires] +python_version = "3.9" diff --git a/examples/monitor_serial_hexbytes/Pipfile.lock b/examples/monitor_serial_hexbytes/Pipfile.lock new file mode 100644 index 0000000..e403acf --- /dev/null +++ b/examples/monitor_serial_hexbytes/Pipfile.lock @@ -0,0 +1,132 @@ +{ + "_meta": { + "hash": { + "sha256": "92287b5b1b591c10b2d90c928fd10ce9b5785fbaeff4cd71a89cb8882b5a2e2b" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "mypy": { + "hashes": [ + "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce", + "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d", + "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069", + "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c", + "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d", + "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714", + "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a", + "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d", + "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05", + "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266", + "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697", + "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc", + "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799", + "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd", + "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00", + "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7", + "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a", + "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0", + "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0", + "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166" + ], + "index": "pypi", + "version": "==0.931" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "result": { + "hashes": [ + "sha256:97dcc4b2c6b376713c8de1b49d583a272e05ea2cd45e7fd18a3e8be42fc9fa6f", + "sha256:bd649435fbf457de32e5e941d63d787c21193b126d48c848458ddda1683de980" + ], + "index": "pypi", + "version": "==0.7.0" + }, + "tomli": { + "hashes": [ + "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224", + "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", + "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" + ], + "markers": "python_version >= '3.6'", + "version": "==4.0.1" + }, + "websockets": { + "hashes": [ + "sha256:002071169d2e44ce8eb9e5ebac9fbce142ba4b5146eef1cfb16b177a27662657", + "sha256:05e7f098c76b0a4743716590bb8f9706de19f1ef5148d61d0cf76495ec3edb9c", + "sha256:08a42856158307e231b199671c4fce52df5786dd3d703f36b5d8ac76b206c485", + "sha256:0d93b7cadc761347d98da12ec1930b5c71b2096f1ceed213973e3cda23fead9c", + "sha256:10edd9d7d3581cfb9ff544ac09fc98cab7ee8f26778a5a8b2d5fd4b0684c5ba5", + "sha256:14e9cf68a08d1a5d42109549201aefba473b1d925d233ae19035c876dd845da9", + "sha256:181d2b25de5a437b36aefedaf006ecb6fa3aa1328ec0236cdde15f32f9d3ff6d", + "sha256:189ed478395967d6a98bb293abf04e8815349e17456a0a15511f1088b6cb26e4", + "sha256:1d858fb31e5ac992a2cdf17e874c95f8a5b1e917e1fb6b45ad85da30734b223f", + "sha256:1dafe98698ece09b8ccba81b910643ff37198e43521d977be76caf37709cf62b", + "sha256:3477146d1f87ead8df0f27e8960249f5248dceb7c2741e8bbec9aa5338d0c053", + "sha256:38db6e2163b021642d0a43200ee2dec8f4980bdbda96db54fde72b283b54cbfc", + "sha256:3a02ab91d84d9056a9ee833c254895421a6333d7ae7fff94b5c68e4fa8095519", + "sha256:3bbf080f3892ba1dc8838786ec02899516a9d227abe14a80ef6fd17d4fb57127", + "sha256:3ef6f73854cded34e78390dbdf40dfdcf0b89b55c0e282468ef92646fce8d13a", + "sha256:468f0031fdbf4d643f89403a66383247eb82803430b14fa27ce2d44d2662ca37", + "sha256:483edee5abed738a0b6a908025be47f33634c2ad8e737edd03ffa895bd600909", + "sha256:531d8eb013a9bc6b3ad101588182aa9b6dd994b190c56df07f0d84a02b85d530", + "sha256:5560558b0dace8312c46aa8915da977db02738ac8ecffbc61acfbfe103e10155", + "sha256:5bb6256de5a4fb1d42b3747b4e2268706c92965d75d0425be97186615bf2f24f", + "sha256:667c41351a6d8a34b53857ceb8343a45c85d438ee4fd835c279591db8aeb85be", + "sha256:6b014875fae19577a392372075e937ebfebf53fd57f613df07b35ab210f31534", + "sha256:6fdec1a0b3e5630c58e3d8704d2011c678929fce90b40908c97dfc47de8dca72", + "sha256:7bdd3d26315db0a9cf8a0af30ca95e0aa342eda9c1377b722e71ccd86bc5d1dd", + "sha256:7c9407719f42cb77049975410490c58a705da6af541adb64716573e550e5c9db", + "sha256:7d6673b2753f9c5377868a53445d0c321ef41ff3c8e3b6d57868e72054bfce5f", + "sha256:816ae7dac2c6522cfa620947ead0ca95ac654916eebf515c94d7c28de5601a6e", + "sha256:882c0b8bdff3bf1bd7f024ce17c6b8006042ec4cceba95cf15df57e57efa471c", + "sha256:8877861e3dee38c8d302eee0d5dbefa6663de3b46dc6a888f70cd7e82562d1f7", + "sha256:888a5fa2a677e0c2b944f9826c756475980f1b276b6302e606f5c4ff5635be9e", + "sha256:89e985d40d407545d5f5e2e58e1fdf19a22bd2d8cd54d20a882e29f97e930a0a", + "sha256:97b4b68a2ddaf5c4707ae79c110bfd874c5be3c6ac49261160fb243fa45d8bbb", + "sha256:98de71f86bdb29430fd7ba9997f47a6b10866800e3ea577598a786a785701bb0", + "sha256:9f304a22ece735a3da8a51309bc2c010e23961a8f675fae46fdf62541ed62123", + "sha256:9fd62c6dc83d5d35fb6a84ff82ec69df8f4657fff05f9cd6c7d9bec0dd57f0f6", + "sha256:a249139abc62ef333e9e85064c27fefb113b16ffc5686cefc315bdaef3eefbc8", + "sha256:b66e6d514f12c28d7a2d80bb2a48ef223342e99c449782d9831b0d29a9e88a17", + "sha256:b68b6caecb9a0c6db537aa79750d1b592a841e4f1a380c6196091e65b2ad35f9", + "sha256:baa83174390c0ff4fc1304fbe24393843ac7a08fdd59295759c4b439e06b1536", + "sha256:bb01ea7b5f52e7125bdc3c5807aeaa2d08a0553979cf2d96a8b7803ea33e15e7", + "sha256:cfae282c2aa7f0c4be45df65c248481f3509f8c40ca8b15ed96c35668ae0ff69", + "sha256:d0d81b46a5c87d443e40ce2272436da8e6092aa91f5fbeb60d1be9f11eff5b4c", + "sha256:d9b245db5a7e64c95816e27d72830e51411c4609c05673d1ae81eb5d23b0be54", + "sha256:ddab2dc69ee5ae27c74dbfe9d7bb6fee260826c136dca257faa1a41d1db61a89", + "sha256:e1b60fd297adb9fc78375778a5220da7f07bf54d2a33ac781319650413fc6a60", + "sha256:e259be0863770cb91b1a6ccf6907f1ac2f07eff0b7f01c249ed751865a70cb0d", + "sha256:e3872ae57acd4306ecf937d36177854e218e999af410a05c17168cd99676c512", + "sha256:e4819c6fb4f336fd5388372cb556b1f3a165f3f68e66913d1a2fc1de55dc6f58" + ], + "index": "pypi", + "version": "==10.1" + } + }, + "develop": {} +} diff --git a/examples/monitor_serial_hexbytes/mock.py b/examples/monitor_serial_hexbytes/mock.py new file mode 100644 index 0000000..0b885d0 --- /dev/null +++ b/examples/monitor_serial_hexbytes/mock.py @@ -0,0 +1,114 @@ +"""Module for mocking serial socket monitoring related""" + +import base64 +from typing import Any, Optional, Generic, TypeVar, Union +from result import Result +from dataclasses import dataclass + +SERIALIZABLE = TypeVar('SERIALIZABLE') +PI = TypeVar('PI') +PO = TypeVar('PO') + + +class Socketlike(Generic[SERIALIZABLE, PI, PO]): + """Interface for interactions w/ sockets""" + async def connect(self) -> Optional[Exception]: + pass + + async def disconnect(self) -> Optional[Exception]: + pass + + async def rx(self) -> Result[PI, Exception]: + pass + + async def tx(self, data: PO) -> Optional[Exception]: + pass + + +@dataclass(frozen=True, eq=False) +class MonitorUnavailable: + """Message regarding hardware monitor unavailability""" + reason: str + + +@dataclass(frozen=True, eq=False) +class SerialMonitorMessageToClient: + """Message from hardware serial monitor to client""" + base64Bytes: str + + @staticmethod + def from_bytes(content: bytes): + """Construct message from bytes""" + return SerialMonitorMessageToClient(base64.b64encode(content).decode("utf-8")) + + def to_bytes(self) -> bytes: + """Construct bytes from message""" + return base64.b64decode(self.base64Bytes) + + +@dataclass(frozen=True, eq=False) +class SerialMonitorMessageToAgent: + """Message from client to hardware serial monitor""" + base64Bytes: str + + def __eq__(self, other) -> bool: + return self.base64Bytes == other.base64Bytes + + @staticmethod + def from_bytes(content: bytes): + """Construct message from bytes""" + return SerialMonitorMessageToAgent(base64.b64encode(content).decode("utf-8")) + + def to_bytes(self): + """Construct bytes from message""" + return base64.b64decode(self.base64Bytes) + + +MonitorListenerIncomingMessage = Union[MonitorUnavailable, SerialMonitorMessageToClient] +MonitorListenerOutgoingMessage = Union[SerialMonitorMessageToAgent] + + +class MonitorSerialHelperLike: + """Helper for managing monitor messages""" + + @staticmethod + def isMonitorUnavailable(message: Any) -> bool: + """Check if message is of type 'MonitorUnavailable'""" + pass + + @staticmethod + def isSerialMonitorMessageToClient(message: Any) -> bool: + """Check if message is of type 'SerialMonitorMessageToClient'""" + pass + + @staticmethod + def isCodecParseException(instance: Any) -> bool: + """Check if class instance is of type 'CodecParseException'""" + pass + + @staticmethod + def createSerialMonitorMessageToAgent(payload: bytes) -> Any: + """Create createSerialMonitorMessageToAgent from bytes""" + pass + + +class MonitorSerial: + """Interface for serial socket monitors""" + + helper: MonitorSerialHelperLike + + def __init__(self, helper): + self.helper = helper + + async def run( + self, + socketlike: Socketlike[Any, MonitorListenerIncomingMessage, MonitorListenerOutgoingMessage] + ): + """Receive serial monitor websocket messages & implement user interfacing""" + pass + + +class CodecParseException(Exception): + """Exception thrown by failing decoders""" + def __eq__(self, other) -> bool: + return str(self) == str(other) diff --git a/examples/monitor_serial_hexbytes/monitor.py b/examples/monitor_serial_hexbytes/monitor.py new file mode 100644 index 0000000..39ddba0 --- /dev/null +++ b/examples/monitor_serial_hexbytes/monitor.py @@ -0,0 +1,158 @@ +"""Module for functionality related to serial socket monitoring""" + +import sys +import asyncio +from asyncio import Task +from typing import Any, Callable, Optional +import termios +import tty +import signal +from pprint import pformat +from result import Ok, Err, Result +from websockets.exceptions import ConnectionClosedError +from functools import partial +from mock import \ + MonitorSerial, \ + Socketlike, \ + MonitorListenerIncomingMessage, \ + MonitorListenerOutgoingMessage, \ + SerialMonitorMessageToClient + + +# Actual monitor implementation +class Death: + """Coroutine-safe application death boolean""" + gracing: bool = False + + def grace(self): + self.gracing = True + + +class MonitorSerialHexbytes(MonitorSerial): + """Serial socket monitor, which sends keyboard keys as bytes & prints incoming data as hex bytes""" + + @staticmethod + def silence_stdin() -> list: + """Stop stdin from immediately being printed back out to stdout""" + stdin = sys.stdin.fileno() + tattr = termios.tcgetattr(stdin) + tty.setcbreak(stdin, termios.TCSANOW) + sys.stdout.write("\x1b[6n") + sys.stdout.flush() + return tattr + + @staticmethod + def unsilence_stdin(tattr: list): + """Allow stdin to be immediately printed back out to stdout""" + stdin = sys.stdin.fileno() + termios.tcsetattr(stdin, termios.TCSANOW, tattr) + + async def keep_transmitting_to_agent( + self, + socketlike: Socketlike[Any, MonitorListenerIncomingMessage, MonitorListenerOutgoingMessage] + ): + """Send keyboard data from stdin straight to serial monitor socket""" + asyncio_loop = asyncio.get_event_loop() + stdin_reader = asyncio.StreamReader() + stdin_protocol = asyncio.StreamReaderProtocol(stdin_reader) + await asyncio_loop.connect_read_pipe(lambda: stdin_protocol, sys.stdin) + while True: + read_bytes = await stdin_reader.read(1) + message = self.helper.createSerialMonitorMessageToAgent(read_bytes) + await socketlike.tx(message) + + @staticmethod + def handle_finish( + socketlike: Socketlike[Any, MonitorListenerIncomingMessage, MonitorListenerOutgoingMessage], + death: Death, + stdin_capture_task: Task, + tattr: list, + ): + asyncio_loop = asyncio.get_event_loop() + death.grace() + stdin_capture_task.cancel() + asyncio_loop.create_task(socketlike.disconnect()) + MonitorSerialHexbytes.unsilence_stdin(tattr) + + @staticmethod + def render_incoming_message(incoming_message: SerialMonitorMessageToClient): + for byte_int in incoming_message.to_bytes(): + render = f"[{hex(byte_int)}:{chr(byte_int)}] " + sys.stdout.buffer.write(str.encode(render)) + sys.stdout.buffer.flush() + + def render_message_data_or_finish( + self, + death: Death, + handle_finish: Callable, + incoming_message_result: Result[MonitorListenerIncomingMessage, Exception] + ) -> Optional[Result[type(True), str]]: + """Handle incoming serial message""" + + # Handle message failures + if death.gracing: + return Ok() + elif isinstance(incoming_message_result, Err) \ + and isinstance(incoming_message_result.value, ConnectionClosedError): + handle_finish() + return Err("Control server connection closed") + if isinstance(incoming_message_result, Err) \ + and self.helper.isCodecParseException(incoming_message_result.value): + handle_finish() + return Err("Unknown command received, ignoring") + elif isinstance(incoming_message_result, Err): + handle_finish() + return Err(f"Failed to receive message: {pformat(incoming_message_result.value, indent=4)}") + + # Handle successful message + incoming_message = incoming_message_result.value + if self.helper.isMonitorUnavailable(incoming_message): + handle_finish() + return Err(f"Monitor not available anymore: {incoming_message.reason}") + elif self.helper.isSerialMonitorMessageToClient(incoming_message): + MonitorSerialHexbytes.render_incoming_message(incoming_message) + return None + else: + handle_finish() + return Err(f"Unknown message received: {incoming_message.reason}") + + async def run( + self, + socketlike: Socketlike[Any, MonitorListenerIncomingMessage, MonitorListenerOutgoingMessage], + ): + """Receive serial monitor websocket messages & implement user interfacing""" + + # Silence stdin + tattr = MonitorSerialHexbytes.silence_stdin() + + # Redirect stdin to serial monitor socket + asyncio_loop = asyncio.get_event_loop() + stdin_capture_task = asyncio_loop.create_task( + self.keep_transmitting_to_agent(socketlike)) + + # Define end-of-monitor handler + death = Death() + handle_finish = partial( + MonitorSerialHexbytes.handle_finish, + socketlike, + death, + stdin_capture_task, + tattr) + + # Handle signal interrupts + for signame in ('SIGINT', 'SIGTERM'): + asyncio_loop.add_signal_handler(getattr(signal, signame), handle_finish) + + # Run monitoring loop + while True: + incoming_message_result = await socketlike.rx() + result = self.render_message_data_or_finish( + death, + handle_finish, + incoming_message_result) + if result is not None: + return result + + +# Export class as 'monitor' for explicit importing +monitor = MonitorSerialHexbytes