Adafruit_Learning_System_Gu.../MacroPad_RPC_Home_Assistant/rpc.py
2023-09-01 10:17:57 -07:00

138 lines
4.3 KiB
Python
Executable file

# SPDX-FileCopyrightText: Copyright (c) 2021 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: Unlicense
"""
USB CDC Remote Procedure Call class
"""
import time
import json
try:
import serial
import adafruit_board_toolkit.circuitpython_serial
json_decode_exception = json.decoder.JSONDecodeError
except ImportError:
import usb_cdc as serial
json_decode_exception = ValueError
RESPONSE_TIMEOUT = 5
DATA_TIMEOUT = 0.5
class RpcError(Exception):
"""For RPC Specific Errors"""
class MqttError(Exception):
"""For MQTT Specific Errors"""
class _Rpc:
def __init__(self):
self._serial = None
@staticmethod
def create_response_packet(
error=False, error_type="RPC", message=None, return_val=None
):
return {
"error": error,
"error_type": error_type if error else None,
"message": message,
"return_val": return_val,
}
@staticmethod
def create_request_packet(function, args=[], kwargs={}): # pylint: disable=dangerous-default-value
return {"function": function, "args": args, "kwargs": kwargs}
def _wait_for_packet(self, timeout=None):
incoming_packet = b""
if timeout is not None:
response_start_time = time.monotonic()
while True:
if incoming_packet:
data_start_time = time.monotonic()
while not self._serial.in_waiting:
if (
incoming_packet
and (time.monotonic() - data_start_time) >= DATA_TIMEOUT
):
incoming_packet = b""
if not incoming_packet and timeout is not None:
if (time.monotonic() - response_start_time) >= timeout:
return self.create_response_packet(
error=True, message="Timed out waiting for response"
)
time.sleep(0.001)
data = self._serial.read(self._serial.in_waiting)
if data:
try:
incoming_packet += data
packet = json.loads(incoming_packet)
# json can try to be clever with missing braces, so make sure we have everything
if sorted(tuple(packet.keys())) == sorted(self._packet_format()):
return packet
except json_decode_exception:
pass # Incomplete packet
class RpcClient(_Rpc):
def __init__(self):
super().__init__()
self._serial = serial.data
def _packet_format(self):
return self.create_response_packet().keys()
def call(self, function, *args, **kwargs):
packet = self.create_request_packet(function, args, kwargs)
self._serial.write(bytes(json.dumps(packet), "utf-8"))
# Wait for response packet to indicate success
return self._wait_for_packet(RESPONSE_TIMEOUT)
class RpcServer(_Rpc):
def __init__(self, handler, baudrate=9600):
super().__init__()
self._serial = self.init_serial(baudrate)
self._handler = handler
def _packet_format(self):
return self.create_request_packet(None).keys()
def init_serial(self, baudrate):
port = self.detect_port()
return serial.Serial(
port,
baudrate,
parity="N",
rtscts=False,
xonxoff=False,
exclusive=True,
)
@staticmethod
def detect_port():
"""
Detect the port automatically
"""
comports = adafruit_board_toolkit.circuitpython_serial.data_comports()
ports = [comport.device for comport in comports]
if len(ports) >= 1:
if len(ports) > 1:
print("Multiple devices detected, using the first detected port.")
return ports[0]
raise RuntimeError(
"Unable to find any CircuitPython Devices with the CDC Data port enabled."
)
def loop(self, timeout=None):
packet = self._wait_for_packet(timeout)
if "error" not in packet:
response_packet = self._handler(packet)
self._serial.write(bytes(json.dumps(response_packet), "utf-8"))
def close_serial(self):
if self._serial is not None:
self._serial.close()