From b6ad253b6cf1ee1a1519b496b8baaa820d8bd6b3 Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Thu, 22 May 2025 13:47:32 +0000 Subject: [PATCH 01/16] Add TCP socket backend for request/response --- .gitignore | 6 +- pyproject.toml | 5 +- python_thingset/backend.py | 1 + python_thingset/can_backend.py | 18 ++- python_thingset/cli.py | 37 ++++- python_thingset/client.py | 6 +- python_thingset/response.py | 10 +- python_thingset/serial_backend.py | 15 +- python_thingset/socket_backend.py | 231 ++++++++++++++++++++++++++++++ python_thingset/thingset.py | 25 +++- 10 files changed, 327 insertions(+), 27 deletions(-) create mode 100644 python_thingset/socket_backend.py diff --git a/.gitignore b/.gitignore index 04ed651..6fb8897 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,8 @@ __pycache__ **/*.log # Mac files -.DS_Store \ No newline at end of file +.DS_Store + +# build metadata / files +*build +python_thingset.egg-info \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 407e4e6..9a05b97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "python_thingset" -version = "0.1.0" +version = "0.2.0" description = "A Python library for ThingSet functionality" authors = [ { name = "Adam Mitchell", email = "adam.mitchell@brillpower.com" } @@ -30,3 +30,6 @@ dev = [ homepage = "https://gitlab.io/Brill-Power/python-thingset/" repository = "https://gitlab.com/Brill-Power/python-thingset/" documentation = "https://gitlab.com/Brill-Power/python-thingset/" + +[project.scripts] +thingset = "python_thingset.cli:run_cli" diff --git a/python_thingset/backend.py b/python_thingset/backend.py index e8613c5..770dd1f 100644 --- a/python_thingset/backend.py +++ b/python_thingset/backend.py @@ -13,6 +13,7 @@ class ThingSetBackend(ABC): CAN: str = "can" Serial: str = "serial" + Socket: str = "socket" def __init__(self): self._running = False diff --git a/python_thingset/can_backend.py b/python_thingset/can_backend.py index 685da83..0854849 100644 --- a/python_thingset/can_backend.py +++ b/python_thingset/can_backend.py @@ -13,11 +13,19 @@ import cbor2 import isotp -from .backend import ThingSetBackend -from .client import ThingSetClient -from .id import ThingSetID -from .log import get_logger -from .response import ThingSetResponse, ThingSetRequest, ThingSetStatus, ThingSetValue +try: + from .backend import ThingSetBackend + from .client import ThingSetClient + from .id import ThingSetID + from .log import get_logger + from .response import ThingSetResponse, ThingSetRequest, ThingSetStatus, ThingSetValue +except ImportError: + from backend import ThingSetBackend + from client import ThingSetClient + from id import ThingSetID + from log import get_logger + from response import ThingSetResponse, ThingSetRequest, ThingSetStatus, ThingSetValue + logger = get_logger() diff --git a/python_thingset/cli.py b/python_thingset/cli.py index 5772575..cc17289 100755 --- a/python_thingset/cli.py +++ b/python_thingset/cli.py @@ -10,8 +10,12 @@ from time import sleep from typing import Union -from .backend import ThingSetBackend -from .thingset import ThingSet +try: + from .backend import ThingSetBackend + from .thingset import ThingSet +except ImportError: + from backend import ThingSetBackend + from thingset import ThingSet def process_args(args: list) -> list: @@ -79,6 +83,9 @@ def setup_args() -> argparse.Namespace: nargs="?", type=str) parent_parser.add_argument("-r", "--baud-rate", help="Specify serial baud rate (example: 115200)", nargs="?", default=115200, type=int) + + """ Socket options """ + group.add_argument("-i", "--ip", help="Specify which IPv4 address to connect to (example 192.0.2.1)") """ Functions """ subparsers = arg_parser.add_subparsers(dest="method", required=True, help="ThingSet function execute " \ @@ -134,9 +141,20 @@ def setup_args() -> argparse.Namespace: args.value = [args.update_args[1]] args.backend = ThingSetBackend.Serial + elif args.ip: + args.backend = ThingSetBackend.Socket - if not (args.can_bus or args.port): - arg_parser.error("One of -c/--can_bus or -p/--port is required") + if args.method == "update": + if len(args.update_args) != 3: + arg_parser.error("When using update with -i/--ip you must suply a parent_id, value_id and value " \ + "(example: thingset update f f03 MyValue -i 192.0.2.1") + else: + args.parent_id = args.update_args[0] + args.value_id = args.update_args[1] + args.value = [args.update_args[2]] + + if not (args.can_bus or args.port or args.ip): + arg_parser.error("One of -c/--can_bus, -i/--ip or -p/--port is required") return args @@ -151,11 +169,15 @@ def run_cli(): case "get": if args.backend.lower() == "serial": response = ts.get(args.id) + elif args.backend.lower() == "socket": + response = ts.get(int(args.id, 16)) else: response = ts.get(int(args.target_address, 16), int(args.id, 16)) case "fetch": if args.backend.lower() == "serial": response = ts.fetch(args.parent_id, args.value_ids) + elif args.backend.lower() == "socket": + response = ts.fetch(int(args.parent_id, 16), [int(i, 16) for i in args.value_ids]) else: response = ts.fetch(int(args.parent_id, 16), [int(i, 16) for i in args.value_ids], int(args.target_address, 16)) case "exec": @@ -163,17 +185,24 @@ def run_cli(): if args.backend.lower() == "serial": response = ts.exec(args.value_id, p_args) + elif args.backend.lower() == "socket": + response = ts.exec(int(args.value_id, 16), p_args) else: response = ts.exec(int(args.value_id, 16), p_args, node_id=int(args.target_address, 16)) case "update": if args.backend.lower() == "serial": response = ts.update(args.parent_id, args.value) + elif args.backend.lower() == "socket": + p_args = process_args(args.value) + response = ts.update(int(args.value_id, 16), p_args[0], parent_id=int(args.parent_id, 16)) else: p_args = process_args(args.value) response = ts.update(int(args.value_id, 16), p_args[0], int(args.target_address, 16), int(args.parent_id, 16)) case "schema": if args.backend.lower() == "serial": get_schema(ts, args.root_id) + elif args.backend.lower() == "socket": + get_schema(ts, int(args.root_id, 16)) else: get_schema(ts, int(args.root_id, 16), int(args.target_address, 16)) case _: diff --git a/python_thingset/client.py b/python_thingset/client.py index 314f352..277aff2 100644 --- a/python_thingset/client.py +++ b/python_thingset/client.py @@ -6,8 +6,10 @@ from abc import ABC, abstractmethod from typing import Any, List, Union -from .response import ThingSetResponse - +try: + from .response import ThingSetResponse +except ImportError: + from response import ThingSetResponse class ThingSetClient(ABC): @abstractmethod diff --git a/python_thingset/response.py b/python_thingset/response.py index af0131f..847eb0a 100644 --- a/python_thingset/response.py +++ b/python_thingset/response.py @@ -9,8 +9,10 @@ import cbor2 -from .backend import ThingSetBackend - +try: + from .backend import ThingSetBackend +except ImportError: + from backend import ThingSetBackend @dataclass class ThingSetStatus(object): @@ -162,7 +164,7 @@ def __init__(self, backend: str, data: Union[bytes, str, None], values: Union[Li if data is not None: match self.backend: - case ThingSetBackend.CAN: + case ThingSetBackend.CAN | ThingSetBackend.Socket: self._process_can(data) case ThingSetBackend.Serial: self._process_serial(data) @@ -205,7 +207,7 @@ def _process_can(self, data: bytes) -> None: def _get_status_byte(self, data: bytes) -> int: match self.backend: - case ThingSetBackend.CAN: + case ThingSetBackend.CAN | ThingSetBackend.Socket: return data[0] case ThingSetBackend.Serial: try: diff --git a/python_thingset/serial_backend.py b/python_thingset/serial_backend.py index 9744de0..1dfbafe 100644 --- a/python_thingset/serial_backend.py +++ b/python_thingset/serial_backend.py @@ -8,10 +8,17 @@ from serial import Serial as PySerial -from .backend import ThingSetBackend -from .client import ThingSetClient -from .log import get_logger -from .response import ThingSetResponse, ThingSetStatus, ThingSetValue +try: + from .backend import ThingSetBackend + from .client import ThingSetClient + from .log import get_logger + from .response import ThingSetResponse, ThingSetStatus, ThingSetValue +except ImportError: + from backend import ThingSetBackend + from client import ThingSetClient + from log import get_logger + from response import ThingSetResponse, ThingSetStatus, ThingSetValue + logger = get_logger() diff --git a/python_thingset/socket_backend.py b/python_thingset/socket_backend.py new file mode 100644 index 0000000..ff6cf9c --- /dev/null +++ b/python_thingset/socket_backend.py @@ -0,0 +1,231 @@ +# +# Copyright (c) 2024-2025 Brill Power. +# +# SPDX-License-Identifier: Apache-2.0 +# +import json +import queue +import socket +import struct +from typing import Any, List, Union + +import cbor2 + +try: + from .backend import ThingSetBackend + from .client import ThingSetClient + from .log import get_logger + from .response import ThingSetResponse, ThingSetRequest, ThingSetStatus, ThingSetValue +except: + from backend import ThingSetBackend + from client import ThingSetClient + from log import get_logger + from response import ThingSetResponse, ThingSetRequest, ThingSetStatus, ThingSetValue + +logger = get_logger() + +class Sock(ThingSetBackend): + PORT = 9001 + + def __init__(self, address: str): + super().__init__() + + self.address = address + + self._queue = queue.Queue() + self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._sock.settimeout(0.1) + + @property + def address(self) -> str: + return self._address + + @address.setter + def address(self, _address) -> None: + self._address = _address + + def get_message(self, timeout: float=0.5) -> Union[bytes, None]: + message = None + try: + message = self._queue.get(timeout=timeout) + except queue.Empty: + pass + finally: + if message is not None: + self._queue.task_done() + # self.disconnect() + return message + + def _handle_message(self, message: bytes) -> None: + self._queue.put(message) + + def connect(self) -> None: + self._sock.connect((self.address, self.PORT)) + self.is_connected = True + self.start_receiving() + + def disconnect(self) -> None: + self.stop_receiving() + self._sock.close() + self.is_connected = False + + def send(self, _data: bytes) -> None: + self._sock.sendall(_data) + + def receive(self) -> bytes: + try: + return self._sock.recv(1024) + except TimeoutError: + pass + + +class ThingSetSock(ThingSetClient): + def __init__(self, address: str="192.0.2.1"): + self.address = address + + self._sock = Sock(address) + self._sock.connect() + self.is_connected = True + + def disconnect(self) -> None: + self._sock.disconnect() + self.is_connected = False + + def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: Union[int, None]=None, get_paths: bool=True) -> ThingSetResponse: + req = bytearray() + req.append(ThingSetRequest.FETCH) + req += cbor2.dumps(parent_id, canonical=True) + + if (len(ids) == 0): + req.append(0xF6) # null + else: + req += cbor2.dumps(ids, canonical=True) + + self._sock.send(req) + msg = self._sock.get_message() + + tmp = ThingSetResponse(ThingSetBackend.Socket, msg) + + values = [] + + if tmp.status_code is not None: + if tmp.status_code <= ThingSetStatus.CONTENT: + """ create ThingSetValue for parent_id if we're getting its children, otherwise + create ThingSetValue for each id in ids + """ + if len(ids) == 0: + values.append(self._create_value(parent_id, tmp.data, get_paths)) + else: + + for idx, id in enumerate(ids): + values.append(self._create_value(id, tmp.data[idx], get_paths)) + + return ThingSetResponse(ThingSetBackend.Socket, msg, values) + + def get(self, value_id: Union[int, str], node_id: Union[int, None]=None) -> ThingSetResponse: + payload = bytes([ThingSetRequest.GET] + list(cbor2.dumps(value_id))) + + self._sock.send(payload) + msg = self._sock.get_message() + + tmp = ThingSetResponse(ThingSetBackend.Socket, msg) + + values = [] + + if tmp.status_code is not None: + if tmp.status_code <= ThingSetStatus.CONTENT: + values.append(ThingSetValue(None, tmp.data, value_id)) + + return ThingSetResponse(ThingSetBackend.Socket, msg, values) + + def exec(self, value_id: Union[int, str], args: Union[Any, None], node_id: Union[int, None]=None) -> ThingSetResponse: + p_args = list() + + for a in args: + if isinstance(a, float): + p_args.append(self._to_f32(a)) + elif isinstance(a, str): + if "true" == a.lower() or "false" == a.lower(): + p_args.append(json.loads(a.lower())) + else: + p_args.append(a) + else: + p_args.append(a) + + payload = bytes([ThingSetRequest.EXEC] + list(cbor2.dumps(value_id)) + list(cbor2.dumps(p_args, canonical=True))) + + self._sock.send(payload) + msg = self._sock.get_message() + + return ThingSetResponse(ThingSetBackend.Socket, msg) + + def update(self, value_id: Union[int, str], value: Any, node_id: Union[int, None]=None, parent_id: Union[int, None]=None) -> ThingSetResponse: + if isinstance(value, float): + value = self._to_f32(value) + if isinstance(value, str): + if "true" == value.lower() or "false" == value.lower(): + value = json.loads(value.lower()) + + payload = bytes([ThingSetRequest.UPDATE] + list(cbor2.dumps(parent_id)) + list(cbor2.dumps({value_id:value}, canonical=True))) + + self._sock.send(payload) + msg = self._sock.get_message() + + return ThingSetResponse(ThingSetBackend.Socket, msg) + + def _to_f32(self, value: float) -> float: + """ In Python, all floats are actually doubles. This does not map well to embedded targets where + there is a clear distinction between the two. + + This function forces the provided floating point argument, value, to its closest 32-bit + representation so that the resultant encoded (CBOR) value is actually a float (not a double) + and can be properly parsed by ThingSet running on an embedded target when expecting a float + """ + return struct.unpack('f', struct.pack('f', value))[0] + + def _create_value(self, value_id: int, value: Any, get_paths: bool=True) -> ThingSetValue: + path = None + + if get_paths: + path = self._get_path(value_id) + + return ThingSetValue(value_id, value, path) + + def _get_path(self, value_id: int) -> str: + if value_id == ThingSetValue.ID_ROOT: + return "Root" + + payload = bytearray([ThingSetRequest.FETCH, 0x17]) + payload.extend(cbor2.dumps([value_id])) + + self._sock.send(payload) + + return ThingSetResponse(ThingSetBackend.Socket, self._sock.get_message).data[0] + + @property + def address(self) -> str: + return self._address + + @address.setter + def address(self, _address) -> None: + self._address = _address + + @property + def is_connected(self) -> bool: + return self._is_connected + + @is_connected.setter + def is_connected(self, _is_connected: bool) -> None: + self._is_connected = _is_connected + + +if __name__ == "__main__": + s = ThingSetSock("192.0.2.1") + + print(s.get(0x300)) + print(s.update(0x300, [77.8], parent_id=0x0)) + print(s.get(0x300)) + print(s.fetch(0, [])) + print(s.exec(0x1000, [4, 5])) + + s.disconnect() diff --git a/python_thingset/thingset.py b/python_thingset/thingset.py index b27c127..af432c1 100755 --- a/python_thingset/thingset.py +++ b/python_thingset/thingset.py @@ -5,16 +5,26 @@ # from typing import Any, List, Union -from .backend import ThingSetBackend -from .can_backend import ThingSetCAN -from .client import ThingSetClient -from .response import ThingSetResponse -from .serial_backend import ThingSetSerial +try: + from .backend import ThingSetBackend + from .can_backend import ThingSetCAN + from .client import ThingSetClient + from .response import ThingSetResponse + from .serial_backend import ThingSetSerial + from .socket_backend import ThingSetSock +except ImportError: + from backend import ThingSetBackend + from can_backend import ThingSetCAN + from client import ThingSetClient + from response import ThingSetResponse + from serial_backend import ThingSetSerial + from socket_backend import ThingSetSock class ThingSet(ThingSetClient): def __init__(self, backend: str="can", can_bus: str="vcan0", can_addr: int=0x00, init_block: bool=True, - source_bus: int=0x00, target_bus: int=0x00, port: str="/dev/pts/5", baud: int=115200) -> "ThingSet": + source_bus: int=0x00, target_bus: int=0x00, port: str="/dev/pts/5", baud: int=115200, + ip_addr: str="192.0.2.1") -> "ThingSet": """ Constructor for ThingSet object Args: @@ -26,6 +36,7 @@ def __init__(self, backend: str="can", can_bus: str="vcan0", can_addr: int=0x00, target_bus: bus number of target bus if using CAN backend port: serial port to connect over if using serial backend baud: serial baud rate if using serial backend + ip_addr: ipv4 address to connect to if using socket backend Returns: instance of a `ThingSet` object @@ -38,6 +49,8 @@ def __init__(self, backend: str="can", can_bus: str="vcan0", can_addr: int=0x00, self.backend = ThingSetCAN(can_bus, can_addr, source_bus=source_bus, target_bus=target_bus) case ThingSetBackend.Serial: self.backend = ThingSetSerial(port, baud) + case ThingSetBackend.Socket: + self.backend = ThingSetSock(ip_addr) case _: raise ValueError(f"Invalid backend specified ({backend})") From 46839c5262657d2a38271dc2e3bd21998c8a6c3d Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Thu, 22 May 2025 13:57:06 +0000 Subject: [PATCH 02/16] whitespace and things --- python_thingset/socket_backend.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/python_thingset/socket_backend.py b/python_thingset/socket_backend.py index ff6cf9c..a373435 100644 --- a/python_thingset/socket_backend.py +++ b/python_thingset/socket_backend.py @@ -22,8 +22,10 @@ from log import get_logger from response import ThingSetResponse, ThingSetRequest, ThingSetStatus, ThingSetValue + logger = get_logger() + class Sock(ThingSetBackend): PORT = 9001 @@ -53,7 +55,7 @@ def get_message(self, timeout: float=0.5) -> Union[bytes, None]: finally: if message is not None: self._queue.task_done() - # self.disconnect() + return message def _handle_message(self, message: bytes) -> None: @@ -219,13 +221,13 @@ def is_connected(self, _is_connected: bool) -> None: self._is_connected = _is_connected -if __name__ == "__main__": - s = ThingSetSock("192.0.2.1") +# if __name__ == "__main__": +# s = ThingSetSock("192.0.2.1") - print(s.get(0x300)) - print(s.update(0x300, [77.8], parent_id=0x0)) - print(s.get(0x300)) - print(s.fetch(0, [])) - print(s.exec(0x1000, [4, 5])) +# print(s.get(0x300)) +# print(s.update(0x300, [77.8], parent_id=0x0)) +# print(s.get(0x300)) +# print(s.fetch(0, [])) +# print(s.exec(0x1000, [4, 5])) - s.disconnect() +# s.disconnect() From 934a057638b49aae826ed71e89e72494a461a57d Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Fri, 23 May 2025 12:16:33 +0000 Subject: [PATCH 03/16] Refactor common code into binary_encoder + various other bits --- python_thingset/binary_encoder.py | 88 ++++++++++++++++++++++++++++ python_thingset/can_backend.py | 95 +++++++------------------------ python_thingset/socket_backend.py | 86 +++++----------------------- 3 files changed, 121 insertions(+), 148 deletions(-) create mode 100644 python_thingset/binary_encoder.py diff --git a/python_thingset/binary_encoder.py b/python_thingset/binary_encoder.py new file mode 100644 index 0000000..aa33ca5 --- /dev/null +++ b/python_thingset/binary_encoder.py @@ -0,0 +1,88 @@ +# +# Copyright (c) 2024-2025 Brill Power. +# +# SPDX-License-Identifier: Apache-2.0 +# +import json +import struct +from typing import Any, List, Union + +import cbor2 + +try: + from .response import ThingSetRequest +except: + from response import ThingSetRequest + + +class ThingSetBinaryEncoder(object): + PATHS = 0x17 + NULL_BYTE = 0xF6 + + def __init__(self): + pass + + """ Fetch request: + 05 # FETCH + F6 # inexplicable null + 02 # CBOR uint: 0x02 (parent ID) + 82 # CBOR array (2 elements) + 18 40 # CBOR uint: 0x40 (object ID) + 18 41 # CBOR uint: 0x41 (object ID) + """ + def encode_fetch(self, parent_id: int, value_ids: List[int]) -> bytes: + req = bytearray() + req.append(ThingSetRequest.FETCH) + req += cbor2.dumps(parent_id, canonical=True) + + if (len(value_ids) == 0): + req.append(self.NULL_BYTE) + else: + req += cbor2.dumps(value_ids, canonical=True) + + return req + + def encode_get(self, value_id: int) -> bytes: + return bytes([ThingSetRequest.GET] + list(cbor2.dumps(value_id))) + + def encode_exec(self, value_id: int, args: Union[Any, None]) -> bytes: + p_args = list() + + for a in args: + if isinstance(a, float): + p_args.append(self.to_f32(a)) + elif isinstance(a, str): + if a.lower() == "true" or a.lower() == "false": + p_args.append(json.loads(a.lower())) + else: + p_args.append(a) + else: + p_args.append(a) + + return bytes([ThingSetRequest.EXEC] + list(cbor2.dumps(value_id)) + list(cbor2.dumps(p_args, canonical=True))) + + def encode_update(self, parent_id: int, value_id: int, value: Any) -> bytes: + if isinstance(value, float): + value = self.to_f32(value) + if isinstance(value, str): + if value.lower() == "true" or value.lower() == "false": + value = json.loads(value.lower()) + + return bytes([ThingSetRequest.UPDATE] + list(cbor2.dumps(parent_id)) + list(cbor2.dumps({value_id:value}, canonical=True))) + + def encode_get_path(self, value_id: int) -> bytes: + req = bytearray([ThingSetRequest.FETCH, self.PATHS]) + req.extend(cbor2.dumps([value_id])) + + return req + + def to_f32(self, value: float) -> float: + """ In Python, all floats are actually doubles. This does not map well to embedded targets where + there is a clear distinction between the two. + + This function forces the provided floating point argument, value, to its closest 32-bit + representation so that the resultant encoded (CBOR) value is actually a float (not a double) + and can be properly parsed by ThingSet running on an embedded target when expecting a float + """ + + return struct.unpack('f', struct.pack('f', value))[0] diff --git a/python_thingset/can_backend.py b/python_thingset/can_backend.py index 0854849..f90b59e 100644 --- a/python_thingset/can_backend.py +++ b/python_thingset/can_backend.py @@ -3,28 +3,27 @@ # # SPDX-License-Identifier: Apache-2.0 # -import json import queue -import struct import threading from typing import Any, Callable, List, Tuple, Union import can -import cbor2 import isotp try: from .backend import ThingSetBackend + from .binary_encoder import ThingSetBinaryEncoder from .client import ThingSetClient from .id import ThingSetID from .log import get_logger - from .response import ThingSetResponse, ThingSetRequest, ThingSetStatus, ThingSetValue + from .response import ThingSetResponse, ThingSetStatus, ThingSetValue except ImportError: from backend import ThingSetBackend + from binary_encoder import ThingSetBinaryEncoder from client import ThingSetClient from id import ThingSetID from log import get_logger - from response import ThingSetResponse, ThingSetRequest, ThingSetStatus, ThingSetValue + from response import ThingSetResponse, ThingSetStatus, ThingSetValue logger = get_logger() @@ -146,7 +145,7 @@ def set_address(self) -> None: self._address = isotp.Address(addressing_mode=isotp.AddressingMode.Normal_29bits, rxid=self.rx_id, txid=self.tx_id) - def get_message(self, timeout: float) -> Union[bytes, None]: + def get_message(self, timeout: float=1.0) -> Union[bytes, None]: message = None try: @@ -202,13 +201,15 @@ def receive(self) -> bytes: return None -class ThingSetCAN(ThingSetClient): +class ThingSetCAN(ThingSetClient, ThingSetBinaryEncoder): ADDR_CLAIM_TIMEOUT_MS: int = 500 CONNECT_TIMEOUT_MS: int = 10000 EUI: list = [0xDE, 0xAD, 0xBE, 0xEF, 0xC0, 0xFF, 0xEE, 0xEE] def __init__(self, bus: str, addr: int = 0x00, source_bus: int=0x00, target_bus: int=0x00): + super().__init__() + self.bus = bus self.node_addr = None self.source_bus = source_bus @@ -230,27 +231,12 @@ def disconnect(self) -> None: self._can.remove_all_rx_filters() - # Request: - # 05 # FETCH - # F6 # inexplicable null - # 02 # CBOR uint: 0x02 (parent ID) - # 82 # CBOR array (2 elements) - # 18 40 # CBOR uint: 0x40 (object ID) - # 18 41 # CBOR uint: 0x41 (object ID) def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: Union[int, None]=None, get_paths: bool=True) -> ThingSetResponse: req_id, resp_id = self._get_isotp_ids(node_id) - req = bytearray() - req.append(ThingSetRequest.FETCH) - req += cbor2.dumps(parent_id, canonical=True) - if (len(ids) == 0): - req.append(0xF6) # null - else: - req += cbor2.dumps(ids, canonical=True) - i = ISOTP(self.bus, resp_id.id, req_id.id) - i.send(req) - msg = i.get_message(1.0) + i.send(self.encode_fetch(parent_id, ids)) + msg = i.get_message() tmp = ThingSetResponse(ThingSetBackend.CAN, msg) @@ -273,11 +259,9 @@ def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: def get(self, node_id: int, value_id: int, get_paths: bool=True) -> ThingSetResponse: req_id, resp_id = self._get_isotp_ids(node_id) - payload = bytes([ThingSetRequest.GET] + list(cbor2.dumps(value_id))) - i = ISOTP(self.bus, resp_id.id, req_id.id) - i.send(payload) - msg = i.get_message(1.0) + i.send(self.encode_get(value_id)) + msg = i.get_message() tmp = ThingSetResponse(ThingSetBackend.CAN, msg) @@ -292,61 +276,23 @@ def get(self, node_id: int, value_id: int, get_paths: bool=True) -> ThingSetResp def exec(self, value_id: Union[int, str], args: Union[Any, None], node_id: Union[int, None]=None) -> ThingSetResponse: req_id, resp_id = self._get_isotp_ids(node_id) - p_args = list() - - for a in args: - if isinstance(a, float): - p_args.append(self._to_f32(a)) - elif isinstance(a, str): - if "true" == a.lower() or "false" == a.lower(): - p_args.append(json.loads(a.lower())) - else: - p_args.append(a) - else: - p_args.append(a) - - payload = bytes([ThingSetRequest.EXEC] + list(cbor2.dumps(value_id)) + list(cbor2.dumps(p_args, canonical=True))) - i = ISOTP(self.bus, resp_id.id, req_id.id) - i.send(payload) - msg = i.get_message(1.0) + i.send(self.encode_exec(value_id, args)) + msg = i.get_message() return ThingSetResponse(ThingSetBackend.CAN, msg) def update(self, value_id: Union[int, str], value: Any, node_id: Union[int, None]=None, parent_id: Union[int, None]=None) -> ThingSetResponse: req_id, resp_id = self._get_isotp_ids(node_id) - if isinstance(value, float): - value = self._to_f32(value) - if isinstance(value, str): - if "true" == value.lower() or "false" == value.lower(): - value = json.loads(value.lower()) - - payload = bytes([ThingSetRequest.UPDATE] + list(cbor2.dumps(parent_id)) + list(cbor2.dumps({value_id:value}, canonical=True))) - i = ISOTP(self.bus, resp_id.id, req_id.id) - i.send(payload) - msg = i.get_message(1.0) + i.send(self.encode_update(parent_id, value_id, value)) + msg = i.get_message() return ThingSetResponse(ThingSetBackend.CAN, msg) - def _to_f32(self, value: float) -> float: - """ In Python, all floats are actually doubles. This does not map well to embedded targets where - there is a clear distinction between the two. - - This function forces the provided floating point argument, value, to its closest 32-bit - representation so that the resultant encoded (CBOR) value is actually a float (not a double) - and can be properly parsed by ThingSet running on an embedded target when expecting a float - """ - return struct.unpack('f', struct.pack('f', value))[0] - def _create_value(self, node_id: int, value_id: int, value: Any, get_paths: bool=True) -> ThingSetValue: - path = None - - if get_paths: - path = self._get_path(node_id, value_id) - - return ThingSetValue(value_id, value, path) + return ThingSetValue(value_id, value, self._get_path(node_id, value_id) if get_paths else None) def _get_path(self, node_id: int, value_id: int) -> str: if value_id == ThingSetValue.ID_ROOT: @@ -354,13 +300,10 @@ def _get_path(self, node_id: int, value_id: int) -> str: req_id, resp_id = self._get_isotp_ids(node_id) - payload = bytearray([ThingSetRequest.FETCH, 0x17]) - payload.extend(cbor2.dumps([value_id])) - i = ISOTP(self.bus, resp_id.id, req_id.id) - i.send(payload) + i.send(self.encode_get_path(value_id)) - return ThingSetResponse(ThingSetBackend.CAN, i.get_message(1.0)).data[0] + return ThingSetResponse(ThingSetBackend.CAN, i.get_message()).data[0] def _get_isotp_ids(self, node_id: int) -> Tuple[ThingSetID]: return ( diff --git a/python_thingset/socket_backend.py b/python_thingset/socket_backend.py index a373435..23c3be7 100644 --- a/python_thingset/socket_backend.py +++ b/python_thingset/socket_backend.py @@ -3,27 +3,20 @@ # # SPDX-License-Identifier: Apache-2.0 # -import json import queue import socket -import struct from typing import Any, List, Union -import cbor2 - try: from .backend import ThingSetBackend + from .binary_encoder import ThingSetBinaryEncoder from .client import ThingSetClient - from .log import get_logger - from .response import ThingSetResponse, ThingSetRequest, ThingSetStatus, ThingSetValue + from .response import ThingSetResponse, ThingSetStatus, ThingSetValue except: from backend import ThingSetBackend + from binary_encoder import ThingSetBinaryEncoder from client import ThingSetClient - from log import get_logger - from response import ThingSetResponse, ThingSetRequest, ThingSetStatus, ThingSetValue - - -logger = get_logger() + from response import ThingSetResponse, ThingSetStatus, ThingSetValue class Sock(ThingSetBackend): @@ -81,8 +74,10 @@ def receive(self) -> bytes: pass -class ThingSetSock(ThingSetClient): +class ThingSetSock(ThingSetClient, ThingSetBinaryEncoder): def __init__(self, address: str="192.0.2.1"): + super().__init__() + self.address = address self._sock = Sock(address) @@ -94,16 +89,7 @@ def disconnect(self) -> None: self.is_connected = False def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: Union[int, None]=None, get_paths: bool=True) -> ThingSetResponse: - req = bytearray() - req.append(ThingSetRequest.FETCH) - req += cbor2.dumps(parent_id, canonical=True) - - if (len(ids) == 0): - req.append(0xF6) # null - else: - req += cbor2.dumps(ids, canonical=True) - - self._sock.send(req) + self._sock.send(self.encode_fetch(parent_id, ids)) msg = self._sock.get_message() tmp = ThingSetResponse(ThingSetBackend.Socket, msg) @@ -118,16 +104,13 @@ def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: if len(ids) == 0: values.append(self._create_value(parent_id, tmp.data, get_paths)) else: - for idx, id in enumerate(ids): values.append(self._create_value(id, tmp.data[idx], get_paths)) return ThingSetResponse(ThingSetBackend.Socket, msg, values) def get(self, value_id: Union[int, str], node_id: Union[int, None]=None) -> ThingSetResponse: - payload = bytes([ThingSetRequest.GET] + list(cbor2.dumps(value_id))) - - self._sock.send(payload) + self._sock.send(self.encode_get(value_id)) msg = self._sock.get_message() tmp = ThingSetResponse(ThingSetBackend.Socket, msg) @@ -141,68 +124,27 @@ def get(self, value_id: Union[int, str], node_id: Union[int, None]=None) -> Thin return ThingSetResponse(ThingSetBackend.Socket, msg, values) def exec(self, value_id: Union[int, str], args: Union[Any, None], node_id: Union[int, None]=None) -> ThingSetResponse: - p_args = list() - - for a in args: - if isinstance(a, float): - p_args.append(self._to_f32(a)) - elif isinstance(a, str): - if "true" == a.lower() or "false" == a.lower(): - p_args.append(json.loads(a.lower())) - else: - p_args.append(a) - else: - p_args.append(a) - - payload = bytes([ThingSetRequest.EXEC] + list(cbor2.dumps(value_id)) + list(cbor2.dumps(p_args, canonical=True))) - - self._sock.send(payload) + self._sock.send(self.encode_exec(value_id, args)) msg = self._sock.get_message() return ThingSetResponse(ThingSetBackend.Socket, msg) def update(self, value_id: Union[int, str], value: Any, node_id: Union[int, None]=None, parent_id: Union[int, None]=None) -> ThingSetResponse: - if isinstance(value, float): - value = self._to_f32(value) - if isinstance(value, str): - if "true" == value.lower() or "false" == value.lower(): - value = json.loads(value.lower()) - - payload = bytes([ThingSetRequest.UPDATE] + list(cbor2.dumps(parent_id)) + list(cbor2.dumps({value_id:value}, canonical=True))) - - self._sock.send(payload) + self._sock.send(self.encode_update(parent_id, value_id, value)) msg = self._sock.get_message() return ThingSetResponse(ThingSetBackend.Socket, msg) - def _to_f32(self, value: float) -> float: - """ In Python, all floats are actually doubles. This does not map well to embedded targets where - there is a clear distinction between the two. - - This function forces the provided floating point argument, value, to its closest 32-bit - representation so that the resultant encoded (CBOR) value is actually a float (not a double) - and can be properly parsed by ThingSet running on an embedded target when expecting a float - """ - return struct.unpack('f', struct.pack('f', value))[0] - def _create_value(self, value_id: int, value: Any, get_paths: bool=True) -> ThingSetValue: - path = None - - if get_paths: - path = self._get_path(value_id) - - return ThingSetValue(value_id, value, path) + return ThingSetValue(value_id, value, self._get_path(value_id) if get_paths else None) def _get_path(self, value_id: int) -> str: if value_id == ThingSetValue.ID_ROOT: return "Root" - payload = bytearray([ThingSetRequest.FETCH, 0x17]) - payload.extend(cbor2.dumps([value_id])) - - self._sock.send(payload) + self._sock.send(self.encode_get_path(value_id)) - return ThingSetResponse(ThingSetBackend.Socket, self._sock.get_message).data[0] + return ThingSetResponse(ThingSetBackend.Socket, self._sock.get_message()).data[0] @property def address(self) -> str: From f2d534fe70e959c52bcfa2d9a66aa906ca1dd43c Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Mon, 26 May 2025 21:16:43 +0000 Subject: [PATCH 04/16] Split responses into text and binary mode --- python_thingset/response.py | 40 ++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/python_thingset/response.py b/python_thingset/response.py index 847eb0a..53d4b10 100644 --- a/python_thingset/response.py +++ b/python_thingset/response.py @@ -154,8 +154,12 @@ def value(self, _value) -> None: class ThingSetResponse(object): + MODE_BIN = 0x1 + MODE_TXT = 0x2 + def __init__(self, backend: str, data: Union[bytes, str, None], values: Union[List[ThingSetValue], None] = None): - self.backend = backend + """ mode is set based on type of backend; binary for CAN or sockets or text for serial """ + self.mode = backend self.status_code = None self.status_string = None @@ -163,13 +167,13 @@ def __init__(self, backend: str, data: Union[bytes, str, None], values: Union[Li self.values = values if data is not None: - match self.backend: - case ThingSetBackend.CAN | ThingSetBackend.Socket: - self._process_can(data) - case ThingSetBackend.Serial: - self._process_serial(data) + match self.mode: + case self.MODE_BIN: + self._process_bin(data) + case self.MODE_TXT: + self._process_txt(data) case _: - raise ValueError(f"Invalid backend ({backend}) specified") + raise ValueError(f"Invalid mode ({self.mode}) specified") def __str__(self) -> str: code = None @@ -179,7 +183,7 @@ def __str__(self) -> str: return f"{code} ({self.status_string}): {self.data}" - def _process_serial(self, data: str) -> None: + def _process_txt(self, data: str) -> None: self._raw_data = data self._processed_data = data.split("\r\n")[0][4:] @@ -192,7 +196,7 @@ def _process_serial(self, data: str) -> None: except json.decoder.JSONDecodeError: pass - def _process_can(self, data: bytes) -> None: + def _process_bin(self, data: bytes) -> None: self._raw_data = data self._processed_data = self._strip_null(self._raw_data) @@ -206,10 +210,10 @@ def _process_can(self, data: bytes) -> None: self.data = e def _get_status_byte(self, data: bytes) -> int: - match self.backend: - case ThingSetBackend.CAN | ThingSetBackend.Socket: + match self.mode: + case self.MODE_BIN: return data[0] - case ThingSetBackend.Serial: + case self.MODE_TXT: try: return int(data[1:3], 16) except ValueError: @@ -218,6 +222,18 @@ def _get_status_byte(self, data: bytes) -> int: def _strip_null(self, data: bytes) -> bytes: return data[1:].replace(b'\xf6', b'') + @property + def mode(self) -> int: + return self._mode + + @mode.setter + def mode(self, _backend) -> None: + match _backend: + case ThingSetBackend.CAN | ThingSetBackend.Socket: + self._mode = self.MODE_BIN + case ThingSetBackend.Serial: + self._mode = self.MODE_TXT + @property def status_code(self) -> int: """ From 6faf58a01b0dc2276d9f9ebc540dfd950a61eea7 Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Mon, 26 May 2025 21:17:06 +0000 Subject: [PATCH 05/16] Factor text encoding out of serial_backend into text_encoder --- python_thingset/serial_backend.py | 88 ++++-------------------------- python_thingset/text_encoder.py | 89 +++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 79 deletions(-) create mode 100644 python_thingset/text_encoder.py diff --git a/python_thingset/serial_backend.py b/python_thingset/serial_backend.py index 1dfbafe..fe343b9 100644 --- a/python_thingset/serial_backend.py +++ b/python_thingset/serial_backend.py @@ -13,11 +13,13 @@ from .client import ThingSetClient from .log import get_logger from .response import ThingSetResponse, ThingSetStatus, ThingSetValue + from .text_encoder import ThingSetTextEncoder except ImportError: from backend import ThingSetBackend from client import ThingSetClient from log import get_logger from response import ThingSetResponse, ThingSetStatus, ThingSetValue + from text_encoder import ThingSetTextEncoder logger = get_logger() @@ -89,8 +91,10 @@ def receive(self) -> bytes: return self._serial.read_until("\n".encode()) -class ThingSetSerial(ThingSetClient): +class ThingSetSerial(ThingSetClient, ThingSetTextEncoder): def __init__(self, port: str="/dev/pts/5", baud=115200): + super().__init__() + self.port = port self.baud = baud @@ -103,19 +107,7 @@ def disconnect(self) -> None: self.is_connected = False def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: Union[int, None]=None) -> ThingSetResponse: - children = "null" - - if len(ids) > 0: - children = "[" - - for i in ids: - children += f'\\"{i}\\",' - - children += "]" - - message = f"thingset ?{parent_id} {children}\n".encode() - - self._serial.send(message) + self._serial.send(self.encode_fetch(parent_id, ids)) msg = self._serial.get_message(.5) tmp = ThingSetResponse(ThingSetBackend.Serial, msg) @@ -134,9 +126,7 @@ def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: return ThingSetResponse(ThingSetBackend.Serial, msg, values) def get(self, value_id: Union[int, str], node_id: Union[int, None]=None) -> ThingSetResponse: - message = f"thingset ?{value_id}\n".encode() - - self._serial.send(message) + self._serial.send(self.encode_get(value_id)) msg = self._serial.get_message(.5) tmp = ThingSetResponse(ThingSetBackend.Serial, msg) @@ -150,73 +140,13 @@ def get(self, value_id: Union[int, str], node_id: Union[int, None]=None) -> Thin return ThingSetResponse(ThingSetBackend.Serial, msg, values) def update(self, value_id: Union[int, str], value: Any, node_id: Union[int, None]=None, parent_id: Union[int, None]=None) -> ThingSetResponse: - """ properly format strings for transmission, add args to stringified list """ - value = value[0] - - val = None - - try: - val = int(value) - except ValueError: - pass - - if val is None: - try: - val = float(value) - except ValueError: - pass - - if val is None: - val = f'\\"{value}\\"' - - path = " " - value_name = None - - path_split = value_id.split("/") - - if len(path_split) > 1: - path = "/".join(path_split[:-1]) + " " - value_name = path_split[-1] - else: - value_name = path_split[0] - - value_path = f'{path}£\\"{value_name}\\":{val}$' - value_path = value_path.replace("£", "{").replace("$", "}") - - message = f"""thingset ={value_path}\n""".encode() - - self._serial.send(message) + self._serial.send(self.encode_update(value_id, value)) msg = self._serial.get_message(0.5) return ThingSetResponse(ThingSetBackend.Serial, msg) def exec(self, value_id: Union[int, str], args: Union[Any, None], node_id: Union[int, None]=None) -> ThingSetResponse: - """ properly format strings for transmission, add args to stringified list """ - processed_args = "[" - - """ leave numeric values as is, surround strings with escape chars """ - for a in args: - try: - int(a) - processed_args += f"{a}," - continue - except ValueError: - pass - - try: - float(a) - processed_args += f"{a}," - continue - except ValueError: - pass - - processed_args += f'\\"{a}\\",' - - processed_args += "]" - - message = f"""thingset !{value_id} {processed_args}\n""".encode() - - self._serial.send(message) + self._serial.send(self.encode_exec(value_id, args)) msg = self._serial.get_message(.5) return ThingSetResponse(ThingSetBackend.Serial, msg) diff --git a/python_thingset/text_encoder.py b/python_thingset/text_encoder.py new file mode 100644 index 0000000..d1f6e46 --- /dev/null +++ b/python_thingset/text_encoder.py @@ -0,0 +1,89 @@ +# +# Copyright (c) 2024-2025 Brill Power. +# +# SPDX-License-Identifier: Apache-2.0 +# +from typing import Any, List, Union + + +class ThingSetTextEncoder(object): + def __init__(self): + pass + + def encode_fetch(self, parent_id: int, ids: List[str]) -> bytes: + children = "null" + + if len(ids) > 0: + children = "[" + + for i in ids: + children += f'\\"{i}\\",' + + children += "]" + + return f"thingset ?{parent_id} {children}\n".encode() + + def encode_get(self, value_id: str) -> bytes: + return f"thingset ?{value_id}\n".encode() + + def encode_exec(self, value_id: str, args: Union[Any, None]) -> bytes: + """ properly format strings for transmission, add args to stringified list """ + processed_args = "[" + + """ leave numeric values as is, surround strings with escape chars """ + for a in args: + try: + int(a) + processed_args += f"{a}," + continue + except ValueError: + pass + + try: + float(a) + processed_args += f"{a}," + continue + except ValueError: + pass + + processed_args += f'\\"{a}\\",' + + processed_args += "]" + + return f"""thingset !{value_id} {processed_args}\n""".encode() + + def encode_update(self, value_id: str, value: Any) -> bytes: + """ properly format strings for transmission, add args to stringified list """ + value = value[0] + + val = None + + try: + val = int(value) + except ValueError: + pass + + if val is None: + try: + val = float(value) + except ValueError: + pass + + if val is None: + val = f'\\"{value}\\"' + + path = " " + value_name = None + + path_split = value_id.split("/") + + if len(path_split) > 1: + path = "/".join(path_split[:-1]) + " " + value_name = path_split[-1] + else: + value_name = path_split[0] + + value_path = f'{path}£\\"{value_name}\\":{val}$' + value_path = value_path.replace("£", "{").replace("$", "}") + + return f"""thingset ={value_path}\n""".encode() From 146ccb5824d54cfedfc139d2c7e9783caa6c4f15 Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Tue, 27 May 2025 10:51:14 +0000 Subject: [PATCH 06/16] Refactor out some more shared bits to save multiple redefinitions --- README.md | 17 ++++++++++++++++- python_thingset/backend.py | 10 ---------- python_thingset/can_backend.py | 12 ------------ python_thingset/client.py | 8 ++++++++ python_thingset/serial_backend.py | 10 ---------- python_thingset/socket_backend.py | 22 ---------------------- 6 files changed, 24 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 072fc48..61a8c68 100644 --- a/README.md +++ b/README.md @@ -151,4 +151,19 @@ thingset exec 66 1.2 2.3 3.55 -c vcan0 -t 2f thingset schema -c vcan0 -t 2f thingset schema f -c vcan0 -t 2f -``` \ No newline at end of file +``` + +#### Socket examples + +``` + if __name__ == "__main__": + s = ThingSetSock("192.0.2.1") + + print(s.get(0x300)) + print(s.update(0x300, [77.8], parent_id=0x0)) + print(s.get(0x300)) + print(s.fetch(0, [])) + print(s.exec(0x1000, [4, 5])) + + s.disconnect() +``` diff --git a/python_thingset/backend.py b/python_thingset/backend.py index 770dd1f..91ea37d 100644 --- a/python_thingset/backend.py +++ b/python_thingset/backend.py @@ -19,16 +19,6 @@ def __init__(self): self._running = False self._thread = None - self.is_connected = False - - @property - def is_connected(self) -> bool: - return self._is_connected - - @is_connected.setter - def is_connected(self, _is_connected) -> None: - self._is_connected = _is_connected - def start_receiving(self) -> None: if not self._running: self._running = True diff --git a/python_thingset/can_backend.py b/python_thingset/can_backend.py index f90b59e..3444ead 100644 --- a/python_thingset/can_backend.py +++ b/python_thingset/can_backend.py @@ -82,14 +82,12 @@ def _handle_message(self, message: can.Message) -> None: def connect(self) -> None: if not self._can: self._can = can.Bus(channel=self.bus, interface=self.interface, fd=self.fd) - self.is_connected = True self.start_receiving() def disconnect(self) -> None: if self._can: self.stop_receiving() self._can.shutdown() - self.is_connected = False def receive(self) -> can.Message: return self._can.recv(timeout=0.1) @@ -163,13 +161,11 @@ def _handle_message(self, message): def connect(self) -> None: self._sock.bind(self.bus, self._address) - self.is_connected = True self.start_receiving() def disconnect(self) -> None: self.stop_receiving() self._sock.close() - self.is_connected = False def send(self, _data: bytes) -> None: """ We have recursive calls to self.send here as we can't easily tell when the CAN @@ -372,11 +368,3 @@ def node_addr(self) -> int: @node_addr.setter def node_addr(self, _addr: Union[int, None]) -> None: self._node_addr = _addr - - @property - def is_connected(self) -> bool: - return self._is_connected - - @is_connected.setter - def is_connected(self, _is_connected: bool) -> None: - self._is_connected = _is_connected diff --git a/python_thingset/client.py b/python_thingset/client.py index 277aff2..4c83783 100644 --- a/python_thingset/client.py +++ b/python_thingset/client.py @@ -31,3 +31,11 @@ def exec(self, value_id: Union[int, str], args: Union[List[Any], None], node_id: @abstractmethod def update(self, value_id: Union[int, str], value: Any, node_id: Union[int, None]=None, parent_id: Union[int, None]=None) -> ThingSetResponse: pass + + @property + def is_connected(self) -> bool: + return self._is_connected + + @is_connected.setter + def is_connected(self, _is_connected: bool) -> None: + self._is_connected = _is_connected diff --git a/python_thingset/serial_backend.py b/python_thingset/serial_backend.py index fe343b9..8cbd7ff 100644 --- a/python_thingset/serial_backend.py +++ b/python_thingset/serial_backend.py @@ -75,14 +75,12 @@ def _handle_message(self, message: bytes) -> None: def connect(self) -> None: if not self._serial: self._serial = PySerial(self.port, self.baud, timeout=.1) - self.is_connected = True self.start_receiving() def disconnect(self) -> None: if self._serial: self.stop_receiving() self._serial.close() - self.is_connected = False def send(self, _data: bytes) -> None: self._serial.write(_data) @@ -166,11 +164,3 @@ def baud(self) -> int: @baud.setter def baud(self, _baud) -> None: self._baud = _baud - - @property - def is_connected(self) -> bool: - return self._is_connected - - @is_connected.setter - def is_connected(self, _is_connected: bool) -> None: - self._is_connected = _is_connected diff --git a/python_thingset/socket_backend.py b/python_thingset/socket_backend.py index 23c3be7..66607c8 100644 --- a/python_thingset/socket_backend.py +++ b/python_thingset/socket_backend.py @@ -56,13 +56,11 @@ def _handle_message(self, message: bytes) -> None: def connect(self) -> None: self._sock.connect((self.address, self.PORT)) - self.is_connected = True self.start_receiving() def disconnect(self) -> None: self.stop_receiving() self._sock.close() - self.is_connected = False def send(self, _data: bytes) -> None: self._sock.sendall(_data) @@ -153,23 +151,3 @@ def address(self) -> str: @address.setter def address(self, _address) -> None: self._address = _address - - @property - def is_connected(self) -> bool: - return self._is_connected - - @is_connected.setter - def is_connected(self, _is_connected: bool) -> None: - self._is_connected = _is_connected - - -# if __name__ == "__main__": -# s = ThingSetSock("192.0.2.1") - -# print(s.get(0x300)) -# print(s.update(0x300, [77.8], parent_id=0x0)) -# print(s.get(0x300)) -# print(s.fetch(0, [])) -# print(s.exec(0x1000, [4, 5])) - -# s.disconnect() From d1f6a39650ef1380c17c3d13a3358332a53f03c6 Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Tue, 27 May 2025 11:26:55 +0000 Subject: [PATCH 07/16] Refactor usage of ThingSet response in ThingSetClients --- python_thingset/can_backend.py | 16 +++++++++------- python_thingset/client.py | 8 ++++++++ python_thingset/serial_backend.py | 14 ++++++++------ python_thingset/socket_backend.py | 16 +++++++++------- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/python_thingset/can_backend.py b/python_thingset/can_backend.py index 3444ead..62f7069 100644 --- a/python_thingset/can_backend.py +++ b/python_thingset/can_backend.py @@ -205,6 +205,8 @@ class ThingSetCAN(ThingSetClient, ThingSetBinaryEncoder): def __init__(self, bus: str, addr: int = 0x00, source_bus: int=0x00, target_bus: int=0x00): super().__init__() + + self.backend = ThingSetBackend.CAN self.bus = bus self.node_addr = None @@ -234,7 +236,7 @@ def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: i.send(self.encode_fetch(parent_id, ids)) msg = i.get_message() - tmp = ThingSetResponse(ThingSetBackend.CAN, msg) + tmp = ThingSetResponse(self.backend, msg) values = [] @@ -250,7 +252,7 @@ def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: for idx, id in enumerate(ids): values.append(self._create_value(node_id, id, tmp.data[idx], get_paths)) - return ThingSetResponse(ThingSetBackend.CAN, msg, values) + return ThingSetResponse(self.backend, msg, values) def get(self, node_id: int, value_id: int, get_paths: bool=True) -> ThingSetResponse: req_id, resp_id = self._get_isotp_ids(node_id) @@ -259,7 +261,7 @@ def get(self, node_id: int, value_id: int, get_paths: bool=True) -> ThingSetResp i.send(self.encode_get(value_id)) msg = i.get_message() - tmp = ThingSetResponse(ThingSetBackend.CAN, msg) + tmp = ThingSetResponse(self.backend, msg) values = [] @@ -267,7 +269,7 @@ def get(self, node_id: int, value_id: int, get_paths: bool=True) -> ThingSetResp if tmp.status_code <= ThingSetStatus.CONTENT: values.append(self._create_value(node_id, value_id, tmp.data, get_paths)) - return ThingSetResponse(ThingSetBackend.CAN, msg, values) + return ThingSetResponse(self.backend, msg, values) def exec(self, value_id: Union[int, str], args: Union[Any, None], node_id: Union[int, None]=None) -> ThingSetResponse: req_id, resp_id = self._get_isotp_ids(node_id) @@ -276,7 +278,7 @@ def exec(self, value_id: Union[int, str], args: Union[Any, None], node_id: Union i.send(self.encode_exec(value_id, args)) msg = i.get_message() - return ThingSetResponse(ThingSetBackend.CAN, msg) + return ThingSetResponse(self.backend, msg) def update(self, value_id: Union[int, str], value: Any, node_id: Union[int, None]=None, parent_id: Union[int, None]=None) -> ThingSetResponse: req_id, resp_id = self._get_isotp_ids(node_id) @@ -285,7 +287,7 @@ def update(self, value_id: Union[int, str], value: Any, node_id: Union[int, None i.send(self.encode_update(parent_id, value_id, value)) msg = i.get_message() - return ThingSetResponse(ThingSetBackend.CAN, msg) + return ThingSetResponse(self.backend, msg) def _create_value(self, node_id: int, value_id: int, value: Any, get_paths: bool=True) -> ThingSetValue: return ThingSetValue(value_id, value, self._get_path(node_id, value_id) if get_paths else None) @@ -299,7 +301,7 @@ def _get_path(self, node_id: int, value_id: int) -> str: i = ISOTP(self.bus, resp_id.id, req_id.id) i.send(self.encode_get_path(value_id)) - return ThingSetResponse(ThingSetBackend.CAN, i.get_message()).data[0] + return ThingSetResponse(self.backend, i.get_message()).data[0] def _get_isotp_ids(self, node_id: int) -> Tuple[ThingSetID]: return ( diff --git a/python_thingset/client.py b/python_thingset/client.py index 4c83783..4fa8981 100644 --- a/python_thingset/client.py +++ b/python_thingset/client.py @@ -39,3 +39,11 @@ def is_connected(self) -> bool: @is_connected.setter def is_connected(self, _is_connected: bool) -> None: self._is_connected = _is_connected + + @property + def backend(self) -> str: + return self._backend + + @backend.setter + def backend(self, _backend) -> None: + self._backend = _backend diff --git a/python_thingset/serial_backend.py b/python_thingset/serial_backend.py index 8cbd7ff..a95a20d 100644 --- a/python_thingset/serial_backend.py +++ b/python_thingset/serial_backend.py @@ -93,6 +93,8 @@ class ThingSetSerial(ThingSetClient, ThingSetTextEncoder): def __init__(self, port: str="/dev/pts/5", baud=115200): super().__init__() + self.backend = ThingSetBackend.Serial + self.port = port self.baud = baud @@ -108,7 +110,7 @@ def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: self._serial.send(self.encode_fetch(parent_id, ids)) msg = self._serial.get_message(.5) - tmp = ThingSetResponse(ThingSetBackend.Serial, msg) + tmp = ThingSetResponse(self.backend, msg) values = [] @@ -121,13 +123,13 @@ def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: for idx, i in enumerate(ids): values.append(ThingSetValue(None, tmp.data[idx], i)) - return ThingSetResponse(ThingSetBackend.Serial, msg, values) + return ThingSetResponse(self.backend, msg, values) def get(self, value_id: Union[int, str], node_id: Union[int, None]=None) -> ThingSetResponse: self._serial.send(self.encode_get(value_id)) msg = self._serial.get_message(.5) - tmp = ThingSetResponse(ThingSetBackend.Serial, msg) + tmp = ThingSetResponse(self.backend, msg) values = [] @@ -135,19 +137,19 @@ def get(self, value_id: Union[int, str], node_id: Union[int, None]=None) -> Thin if tmp.status_code <= ThingSetStatus.CONTENT: values.append(ThingSetValue(None, tmp.data, value_id)) - return ThingSetResponse(ThingSetBackend.Serial, msg, values) + return ThingSetResponse(self.backend, msg, values) def update(self, value_id: Union[int, str], value: Any, node_id: Union[int, None]=None, parent_id: Union[int, None]=None) -> ThingSetResponse: self._serial.send(self.encode_update(value_id, value)) msg = self._serial.get_message(0.5) - return ThingSetResponse(ThingSetBackend.Serial, msg) + return ThingSetResponse(self.backend, msg) def exec(self, value_id: Union[int, str], args: Union[Any, None], node_id: Union[int, None]=None) -> ThingSetResponse: self._serial.send(self.encode_exec(value_id, args)) msg = self._serial.get_message(.5) - return ThingSetResponse(ThingSetBackend.Serial, msg) + return ThingSetResponse(self.backend, msg) @property def port(self) -> str: diff --git a/python_thingset/socket_backend.py b/python_thingset/socket_backend.py index 66607c8..a3dc7f3 100644 --- a/python_thingset/socket_backend.py +++ b/python_thingset/socket_backend.py @@ -76,6 +76,8 @@ class ThingSetSock(ThingSetClient, ThingSetBinaryEncoder): def __init__(self, address: str="192.0.2.1"): super().__init__() + self.backend = ThingSetBackend.Socket + self.address = address self._sock = Sock(address) @@ -90,7 +92,7 @@ def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: self._sock.send(self.encode_fetch(parent_id, ids)) msg = self._sock.get_message() - tmp = ThingSetResponse(ThingSetBackend.Socket, msg) + tmp = ThingSetResponse(self.backend, msg) values = [] @@ -105,13 +107,13 @@ def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: for idx, id in enumerate(ids): values.append(self._create_value(id, tmp.data[idx], get_paths)) - return ThingSetResponse(ThingSetBackend.Socket, msg, values) + return ThingSetResponse(self.backend, msg, values) def get(self, value_id: Union[int, str], node_id: Union[int, None]=None) -> ThingSetResponse: self._sock.send(self.encode_get(value_id)) msg = self._sock.get_message() - tmp = ThingSetResponse(ThingSetBackend.Socket, msg) + tmp = ThingSetResponse(self.backend, msg) values = [] @@ -119,19 +121,19 @@ def get(self, value_id: Union[int, str], node_id: Union[int, None]=None) -> Thin if tmp.status_code <= ThingSetStatus.CONTENT: values.append(ThingSetValue(None, tmp.data, value_id)) - return ThingSetResponse(ThingSetBackend.Socket, msg, values) + return ThingSetResponse(self.backend, msg, values) def exec(self, value_id: Union[int, str], args: Union[Any, None], node_id: Union[int, None]=None) -> ThingSetResponse: self._sock.send(self.encode_exec(value_id, args)) msg = self._sock.get_message() - return ThingSetResponse(ThingSetBackend.Socket, msg) + return ThingSetResponse(self.backend, msg) def update(self, value_id: Union[int, str], value: Any, node_id: Union[int, None]=None, parent_id: Union[int, None]=None) -> ThingSetResponse: self._sock.send(self.encode_update(parent_id, value_id, value)) msg = self._sock.get_message() - return ThingSetResponse(ThingSetBackend.Socket, msg) + return ThingSetResponse(self.backend, msg) def _create_value(self, value_id: int, value: Any, get_paths: bool=True) -> ThingSetValue: return ThingSetValue(value_id, value, self._get_path(value_id) if get_paths else None) @@ -142,7 +144,7 @@ def _get_path(self, value_id: int) -> str: self._sock.send(self.encode_get_path(value_id)) - return ThingSetResponse(ThingSetBackend.Socket, self._sock.get_message()).data[0] + return ThingSetResponse(self.backend, self._sock.get_message()).data[0] @property def address(self) -> str: From dc365034887f349d73afbb133dd38410871f46bf Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Tue, 27 May 2025 16:24:06 +0000 Subject: [PATCH 08/16] Big refactor, lots of stuff, incomplete --- python_thingset/can_backend.py | 82 +++---------------------------- python_thingset/cli.py | 2 +- python_thingset/client.py | 77 ++++++++++++++++++++++++----- python_thingset/serial_backend.py | 24 ++++----- python_thingset/socket_backend.py | 65 ++---------------------- python_thingset/text_encoder.py | 2 +- python_thingset/thingset.py | 6 +-- 7 files changed, 90 insertions(+), 168 deletions(-) diff --git a/python_thingset/can_backend.py b/python_thingset/can_backend.py index 62f7069..b30df81 100644 --- a/python_thingset/can_backend.py +++ b/python_thingset/can_backend.py @@ -5,7 +5,7 @@ # import queue import threading -from typing import Any, Callable, List, Tuple, Union +from typing import Callable, Tuple, Union import can import isotp @@ -16,14 +16,12 @@ from .client import ThingSetClient from .id import ThingSetID from .log import get_logger - from .response import ThingSetResponse, ThingSetStatus, ThingSetValue except ImportError: from backend import ThingSetBackend from binary_encoder import ThingSetBinaryEncoder from client import ThingSetClient from id import ThingSetID from log import get_logger - from response import ThingSetResponse, ThingSetStatus, ThingSetValue logger = get_logger() @@ -207,7 +205,6 @@ def __init__(self, bus: str, addr: int = 0x00, source_bus: int=0x00, target_bus: super().__init__() self.backend = ThingSetBackend.CAN - self.bus = bus self.node_addr = None self.source_bus = source_bus @@ -218,7 +215,6 @@ def __init__(self, bus: str, addr: int = 0x00, source_bus: int=0x00, target_bus: self._can = CAN(self.bus) self._can.connect() - self._negotiate_address(addr) def disconnect(self) -> None: @@ -229,79 +225,13 @@ def disconnect(self) -> None: self._can.remove_all_rx_filters() - def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: Union[int, None]=None, get_paths: bool=True) -> ThingSetResponse: - req_id, resp_id = self._get_isotp_ids(node_id) - - i = ISOTP(self.bus, resp_id.id, req_id.id) - i.send(self.encode_fetch(parent_id, ids)) - msg = i.get_message() - - tmp = ThingSetResponse(self.backend, msg) - - values = [] - - if tmp.status_code is not None: - if tmp.status_code <= ThingSetStatus.CONTENT: - """ create ThingSetValue for parent_id if we're getting its children, otherwise - create ThingSetValue for each id in ids - """ - if len(ids) == 0: - values.append(self._create_value(node_id, parent_id, tmp.data, get_paths)) - else: - - for idx, id in enumerate(ids): - values.append(self._create_value(node_id, id, tmp.data[idx], get_paths)) - - return ThingSetResponse(self.backend, msg, values) - - def get(self, node_id: int, value_id: int, get_paths: bool=True) -> ThingSetResponse: + def _send(self, data: bytes, node_id: Union[int, None]) -> None: req_id, resp_id = self._get_isotp_ids(node_id) + self._isotp = ISOTP(self.bus, resp_id.id, req_id.id) + self._isotp.send(data) - i = ISOTP(self.bus, resp_id.id, req_id.id) - i.send(self.encode_get(value_id)) - msg = i.get_message() - - tmp = ThingSetResponse(self.backend, msg) - - values = [] - - if tmp.status_code is not None: - if tmp.status_code <= ThingSetStatus.CONTENT: - values.append(self._create_value(node_id, value_id, tmp.data, get_paths)) - - return ThingSetResponse(self.backend, msg, values) - - def exec(self, value_id: Union[int, str], args: Union[Any, None], node_id: Union[int, None]=None) -> ThingSetResponse: - req_id, resp_id = self._get_isotp_ids(node_id) - - i = ISOTP(self.bus, resp_id.id, req_id.id) - i.send(self.encode_exec(value_id, args)) - msg = i.get_message() - - return ThingSetResponse(self.backend, msg) - - def update(self, value_id: Union[int, str], value: Any, node_id: Union[int, None]=None, parent_id: Union[int, None]=None) -> ThingSetResponse: - req_id, resp_id = self._get_isotp_ids(node_id) - - i = ISOTP(self.bus, resp_id.id, req_id.id) - i.send(self.encode_update(parent_id, value_id, value)) - msg = i.get_message() - - return ThingSetResponse(self.backend, msg) - - def _create_value(self, node_id: int, value_id: int, value: Any, get_paths: bool=True) -> ThingSetValue: - return ThingSetValue(value_id, value, self._get_path(node_id, value_id) if get_paths else None) - - def _get_path(self, node_id: int, value_id: int) -> str: - if value_id == ThingSetValue.ID_ROOT: - return "Root" - - req_id, resp_id = self._get_isotp_ids(node_id) - - i = ISOTP(self.bus, resp_id.id, req_id.id) - i.send(self.encode_get_path(value_id)) - - return ThingSetResponse(self.backend, i.get_message()).data[0] + def _recv(self) -> bytes: + return self._isotp.get_message() def _get_isotp_ids(self, node_id: int) -> Tuple[ThingSetID]: return ( diff --git a/python_thingset/cli.py b/python_thingset/cli.py index cc17289..4ae95cd 100755 --- a/python_thingset/cli.py +++ b/python_thingset/cli.py @@ -172,7 +172,7 @@ def run_cli(): elif args.backend.lower() == "socket": response = ts.get(int(args.id, 16)) else: - response = ts.get(int(args.target_address, 16), int(args.id, 16)) + response = ts.get(int(args.id, 16), int(args.target_address, 16)) case "fetch": if args.backend.lower() == "serial": response = ts.fetch(args.parent_id, args.value_ids) diff --git a/python_thingset/client.py b/python_thingset/client.py index 4fa8981..d0fa33b 100644 --- a/python_thingset/client.py +++ b/python_thingset/client.py @@ -7,29 +7,84 @@ from typing import Any, List, Union try: - from .response import ThingSetResponse + from .response import ThingSetResponse, ThingSetStatus, ThingSetValue + from .log import get_logger except ImportError: - from response import ThingSetResponse + from response import ThingSetResponse, ThingSetStatus, ThingSetValue + from log import get_logger + + +logger = get_logger() + class ThingSetClient(ABC): - @abstractmethod - def disconnect(self) -> None: - pass + def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: Union[int, None]=None, get_paths: bool=True) -> ThingSetResponse: + values = [] - @abstractmethod - def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: Union[int, None]=None) -> ThingSetResponse: - pass + self._send(self.encode_fetch(parent_id, ids), node_id) + + msg = self._recv() + tmp = ThingSetResponse(self.backend, msg) + + if tmp.status_code is not None: + if tmp.status_code <= ThingSetStatus.CONTENT: + if len(ids) == 0: + values.append(self._create_value(parent_id, node_id, tmp.data, get_paths)) + else: + for idx, id in enumerate(ids): + values.append(self._create_value(id, node_id, tmp.data[idx], get_paths)) + + return ThingSetResponse(self.backend, msg, values) + + def get(self, value_id: Union[int, str], node_id: Union[int, None]=None, get_paths: bool=True) -> ThingSetResponse: + values = [] + + self._send(self.encode_get(value_id), node_id) + + msg = self._recv() + tmp = ThingSetResponse(self.backend, msg) + + if tmp.status_code is not None: + if tmp.status_code <= ThingSetStatus.CONTENT: + values.append(self._create_value(value_id, node_id, tmp.data, get_paths)) + + return ThingSetResponse(self.backend, msg, values) + + def update(self, value_id: Union[int, str], value: Any, node_id: Union[int, None]=None, parent_id: Union[int, None]=None) -> ThingSetResponse: + self._send(self.encode_update(parent_id, value_id, value), node_id) + return ThingSetResponse(self.backend, self._recv()) + + def exec(self, value_id: Union[int, str], args: Union[List[Any], None], node_id: Union[int, None]=None) -> ThingSetResponse: + self._send(self.encode_exec(value_id, args), node_id) + return ThingSetResponse(self.backend, self._recv()) + + def _create_value(self, value_id: int, node_id: int, value: Any, get_paths: bool) -> ThingSetValue: + path = None + + if get_paths: + if value_id == ThingSetValue.ID_ROOT: + path = "Root" + else: + self._send(self.encode_get_path(value_id), node_id) + tmp = ThingSetResponse(self.backend, self._recv()) + + if tmp.data is not None: + path = tmp.data[0] + else: + logger.warning("Failed to read value path") + + return ThingSetValue(value_id, value, path) @abstractmethod - def get(self, value_id: Union[int, str], node_id: Union[int, None]=None) -> ThingSetResponse: + def disconnect(self) -> None: pass @abstractmethod - def exec(self, value_id: Union[int, str], args: Union[List[Any], None], node_id: Union[int, None]=None) -> ThingSetResponse: + def _send(self, data: bytes, node_id: Union[int, None]) -> None: pass @abstractmethod - def update(self, value_id: Union[int, str], value: Any, node_id: Union[int, None]=None, parent_id: Union[int, None]=None) -> ThingSetResponse: + def _recv(self) -> bytes: pass @property diff --git a/python_thingset/serial_backend.py b/python_thingset/serial_backend.py index a95a20d..77fd5ed 100644 --- a/python_thingset/serial_backend.py +++ b/python_thingset/serial_backend.py @@ -50,7 +50,7 @@ def baud(self) -> int: def baud(self, _baud) -> None: self._baud = _baud - def get_message(self, timeout: float) -> Union[str, None]: + def get_message(self, timeout: float=0.5) -> Union[str, None]: message = None try: @@ -108,7 +108,7 @@ def disconnect(self) -> None: def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: Union[int, None]=None) -> ThingSetResponse: self._serial.send(self.encode_fetch(parent_id, ids)) - msg = self._serial.get_message(.5) + msg = self._serial.get_message() tmp = ThingSetResponse(self.backend, msg) @@ -125,9 +125,9 @@ def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: return ThingSetResponse(self.backend, msg, values) - def get(self, value_id: Union[int, str], node_id: Union[int, None]=None) -> ThingSetResponse: + def get(self, value_id: Union[int, str], node_id: Union[int, None]=None, get_paths: bool=False) -> ThingSetResponse: self._serial.send(self.encode_get(value_id)) - msg = self._serial.get_message(.5) + msg = self._serial.get_message() tmp = ThingSetResponse(self.backend, msg) @@ -138,18 +138,12 @@ def get(self, value_id: Union[int, str], node_id: Union[int, None]=None) -> Thin values.append(ThingSetValue(None, tmp.data, value_id)) return ThingSetResponse(self.backend, msg, values) + + def _send(self, data: bytes, _: Union[int, None]) -> None: + self._serial.send(data) - def update(self, value_id: Union[int, str], value: Any, node_id: Union[int, None]=None, parent_id: Union[int, None]=None) -> ThingSetResponse: - self._serial.send(self.encode_update(value_id, value)) - msg = self._serial.get_message(0.5) - - return ThingSetResponse(self.backend, msg) - - def exec(self, value_id: Union[int, str], args: Union[Any, None], node_id: Union[int, None]=None) -> ThingSetResponse: - self._serial.send(self.encode_exec(value_id, args)) - msg = self._serial.get_message(.5) - - return ThingSetResponse(self.backend, msg) + def _recv(self) -> bytes: + self._serial.get_message() @property def port(self) -> str: diff --git a/python_thingset/socket_backend.py b/python_thingset/socket_backend.py index a3dc7f3..d306380 100644 --- a/python_thingset/socket_backend.py +++ b/python_thingset/socket_backend.py @@ -5,18 +5,16 @@ # import queue import socket -from typing import Any, List, Union +from typing import Union try: from .backend import ThingSetBackend from .binary_encoder import ThingSetBinaryEncoder from .client import ThingSetClient - from .response import ThingSetResponse, ThingSetStatus, ThingSetValue except: from backend import ThingSetBackend from binary_encoder import ThingSetBinaryEncoder from client import ThingSetClient - from response import ThingSetResponse, ThingSetStatus, ThingSetValue class Sock(ThingSetBackend): @@ -77,7 +75,6 @@ def __init__(self, address: str="192.0.2.1"): super().__init__() self.backend = ThingSetBackend.Socket - self.address = address self._sock = Sock(address) @@ -88,63 +85,11 @@ def disconnect(self) -> None: self._sock.disconnect() self.is_connected = False - def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: Union[int, None]=None, get_paths: bool=True) -> ThingSetResponse: - self._sock.send(self.encode_fetch(parent_id, ids)) - msg = self._sock.get_message() - - tmp = ThingSetResponse(self.backend, msg) - - values = [] - - if tmp.status_code is not None: - if tmp.status_code <= ThingSetStatus.CONTENT: - """ create ThingSetValue for parent_id if we're getting its children, otherwise - create ThingSetValue for each id in ids - """ - if len(ids) == 0: - values.append(self._create_value(parent_id, tmp.data, get_paths)) - else: - for idx, id in enumerate(ids): - values.append(self._create_value(id, tmp.data[idx], get_paths)) - - return ThingSetResponse(self.backend, msg, values) - - def get(self, value_id: Union[int, str], node_id: Union[int, None]=None) -> ThingSetResponse: - self._sock.send(self.encode_get(value_id)) - msg = self._sock.get_message() - - tmp = ThingSetResponse(self.backend, msg) - - values = [] - - if tmp.status_code is not None: - if tmp.status_code <= ThingSetStatus.CONTENT: - values.append(ThingSetValue(None, tmp.data, value_id)) - - return ThingSetResponse(self.backend, msg, values) - - def exec(self, value_id: Union[int, str], args: Union[Any, None], node_id: Union[int, None]=None) -> ThingSetResponse: - self._sock.send(self.encode_exec(value_id, args)) - msg = self._sock.get_message() - - return ThingSetResponse(self.backend, msg) - - def update(self, value_id: Union[int, str], value: Any, node_id: Union[int, None]=None, parent_id: Union[int, None]=None) -> ThingSetResponse: - self._sock.send(self.encode_update(parent_id, value_id, value)) - msg = self._sock.get_message() - - return ThingSetResponse(self.backend, msg) - - def _create_value(self, value_id: int, value: Any, get_paths: bool=True) -> ThingSetValue: - return ThingSetValue(value_id, value, self._get_path(value_id) if get_paths else None) - - def _get_path(self, value_id: int) -> str: - if value_id == ThingSetValue.ID_ROOT: - return "Root" - - self._sock.send(self.encode_get_path(value_id)) + def _send(self, data: bytes, _: Union[int, None]) -> None: + self._sock.send(data) - return ThingSetResponse(self.backend, self._sock.get_message()).data[0] + def _recv(self) -> bytes: + return self._sock.get_message() @property def address(self) -> str: diff --git a/python_thingset/text_encoder.py b/python_thingset/text_encoder.py index d1f6e46..d38e689 100644 --- a/python_thingset/text_encoder.py +++ b/python_thingset/text_encoder.py @@ -52,7 +52,7 @@ def encode_exec(self, value_id: str, args: Union[Any, None]) -> bytes: return f"""thingset !{value_id} {processed_args}\n""".encode() - def encode_update(self, value_id: str, value: Any) -> bytes: + def encode_update(self, parent_id: None, value_id: str, value: Any) -> bytes: """ properly format strings for transmission, add args to stringified list """ value = value[0] diff --git a/python_thingset/thingset.py b/python_thingset/thingset.py index af432c1..8f3bba9 100755 --- a/python_thingset/thingset.py +++ b/python_thingset/thingset.py @@ -8,20 +8,18 @@ try: from .backend import ThingSetBackend from .can_backend import ThingSetCAN - from .client import ThingSetClient from .response import ThingSetResponse from .serial_backend import ThingSetSerial from .socket_backend import ThingSetSock except ImportError: from backend import ThingSetBackend from can_backend import ThingSetCAN - from client import ThingSetClient from response import ThingSetResponse from serial_backend import ThingSetSerial from socket_backend import ThingSetSock -class ThingSet(ThingSetClient): +class ThingSet(object): def __init__(self, backend: str="can", can_bus: str="vcan0", can_addr: int=0x00, init_block: bool=True, source_bus: int=0x00, target_bus: int=0x00, port: str="/dev/pts/5", baud: int=115200, ip_addr: str="192.0.2.1") -> "ThingSet": @@ -73,7 +71,7 @@ def disconnect(self) -> None: if self.backend is not None: return self.backend.disconnect() - def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: Union[int, None]=None) -> "ThingSetResponse": + def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: Union[int, None]=None) -> ThingSetResponse: """ Perform a ThingSet fetch request Args: From 800a754339e6eed79d6449453b34f6d1ab682b84 Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Tue, 27 May 2025 20:14:34 +0000 Subject: [PATCH 09/16] Refactor serial client --- python_thingset/client.py | 17 ++++++++++--- python_thingset/serial_backend.py | 40 ++----------------------------- 2 files changed, 16 insertions(+), 41 deletions(-) diff --git a/python_thingset/client.py b/python_thingset/client.py index d0fa33b..eba33f7 100644 --- a/python_thingset/client.py +++ b/python_thingset/client.py @@ -7,9 +7,11 @@ from typing import Any, List, Union try: + from .backend import ThingSetBackend from .response import ThingSetResponse, ThingSetStatus, ThingSetValue from .log import get_logger except ImportError: + from backend import ThingSetBackend from response import ThingSetResponse, ThingSetStatus, ThingSetValue from log import get_logger @@ -29,10 +31,16 @@ def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: if tmp.status_code is not None: if tmp.status_code <= ThingSetStatus.CONTENT: if len(ids) == 0: - values.append(self._create_value(parent_id, node_id, tmp.data, get_paths)) + if self.backend == ThingSetBackend.Serial: + values.append(ThingSetValue(None, tmp.data, parent_id)) + else: + values.append(self._create_value(parent_id, node_id, tmp.data, get_paths)) else: for idx, id in enumerate(ids): - values.append(self._create_value(id, node_id, tmp.data[idx], get_paths)) + if self.backend == ThingSetBackend.Serial: + values.append(ThingSetValue(None, tmp.data[idx], id)) + else: + values.append(self._create_value(id, node_id, tmp.data[idx], get_paths)) return ThingSetResponse(self.backend, msg, values) @@ -46,7 +54,10 @@ def get(self, value_id: Union[int, str], node_id: Union[int, None]=None, get_pat if tmp.status_code is not None: if tmp.status_code <= ThingSetStatus.CONTENT: - values.append(self._create_value(value_id, node_id, tmp.data, get_paths)) + if self.backend == ThingSetBackend.Serial: + values.append(ThingSetValue(None, tmp.data, value_id)) + else: + values.append(self._create_value(value_id, node_id, tmp.data, get_paths)) return ThingSetResponse(self.backend, msg, values) diff --git a/python_thingset/serial_backend.py b/python_thingset/serial_backend.py index 77fd5ed..ff8d412 100644 --- a/python_thingset/serial_backend.py +++ b/python_thingset/serial_backend.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: Apache-2.0 # import queue -from typing import Any, List, Union +from typing import Union from serial import Serial as PySerial @@ -12,18 +12,17 @@ from .backend import ThingSetBackend from .client import ThingSetClient from .log import get_logger - from .response import ThingSetResponse, ThingSetStatus, ThingSetValue from .text_encoder import ThingSetTextEncoder except ImportError: from backend import ThingSetBackend from client import ThingSetClient from log import get_logger - from response import ThingSetResponse, ThingSetStatus, ThingSetValue from text_encoder import ThingSetTextEncoder logger = get_logger() + class Serial(ThingSetBackend): def __init__(self, port: str="/dev/pts/5", baud=115200): super().__init__() @@ -66,7 +65,6 @@ def get_message(self, timeout: float=0.5) -> Union[str, None]: def _handle_message(self, message: bytes) -> None: decoded = message.decode() - """ if you want to print everything that happens on the shell, uncomment below """ logger.debug(decoded) if not decoded.startswith("thingset") and not decoded.startswith("uart") and not decoded.startswith("\x1b"): @@ -94,7 +92,6 @@ def __init__(self, port: str="/dev/pts/5", baud=115200): super().__init__() self.backend = ThingSetBackend.Serial - self.port = port self.baud = baud @@ -105,39 +102,6 @@ def __init__(self, port: str="/dev/pts/5", baud=115200): def disconnect(self) -> None: self._serial.disconnect() self.is_connected = False - - def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: Union[int, None]=None) -> ThingSetResponse: - self._serial.send(self.encode_fetch(parent_id, ids)) - msg = self._serial.get_message() - - tmp = ThingSetResponse(self.backend, msg) - - values = [] - - if tmp.status_code is not None: - if tmp.status_code <= ThingSetStatus.CONTENT: - - if len(ids) == 0: - values.append(ThingSetValue(None, tmp.data, parent_id)) - else: - for idx, i in enumerate(ids): - values.append(ThingSetValue(None, tmp.data[idx], i)) - - return ThingSetResponse(self.backend, msg, values) - - def get(self, value_id: Union[int, str], node_id: Union[int, None]=None, get_paths: bool=False) -> ThingSetResponse: - self._serial.send(self.encode_get(value_id)) - msg = self._serial.get_message() - - tmp = ThingSetResponse(self.backend, msg) - - values = [] - - if tmp.status_code is not None: - if tmp.status_code <= ThingSetStatus.CONTENT: - values.append(ThingSetValue(None, tmp.data, value_id)) - - return ThingSetResponse(self.backend, msg, values) def _send(self, data: bytes, _: Union[int, None]) -> None: self._serial.send(data) From 465cad102ed4418b87e219efb958288a9cec19bc Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Tue, 27 May 2025 20:27:29 +0000 Subject: [PATCH 10/16] Refactor imports --- python_thingset/{ => backends}/backend.py | 0 .../{can_backend.py => backends/can.py} | 18 ++++++------------ .../{serial_backend.py => backends/serial.py} | 14 ++++---------- .../{socket_backend.py => backends/socket.py} | 11 +++-------- python_thingset/cli.py | 8 ++------ python_thingset/client.py | 11 +++-------- .../{binary_encoder.py => encoders/binary.py} | 5 +---- .../{text_encoder.py => encoders/text.py} | 0 python_thingset/log.py | 6 +++++- python_thingset/response.py | 5 +---- python_thingset/thingset.py | 17 +++++------------ 11 files changed, 30 insertions(+), 65 deletions(-) rename python_thingset/{ => backends}/backend.py (100%) rename python_thingset/{can_backend.py => backends/can.py} (95%) rename python_thingset/{serial_backend.py => backends/serial.py} (88%) rename python_thingset/{socket_backend.py => backends/socket.py} (88%) rename python_thingset/{binary_encoder.py => encoders/binary.py} (96%) rename python_thingset/{text_encoder.py => encoders/text.py} (100%) diff --git a/python_thingset/backend.py b/python_thingset/backends/backend.py similarity index 100% rename from python_thingset/backend.py rename to python_thingset/backends/backend.py diff --git a/python_thingset/can_backend.py b/python_thingset/backends/can.py similarity index 95% rename from python_thingset/can_backend.py rename to python_thingset/backends/can.py index b30df81..2c83b42 100644 --- a/python_thingset/can_backend.py +++ b/python_thingset/backends/can.py @@ -10,22 +10,16 @@ import can import isotp -try: - from .backend import ThingSetBackend - from .binary_encoder import ThingSetBinaryEncoder - from .client import ThingSetClient - from .id import ThingSetID - from .log import get_logger -except ImportError: - from backend import ThingSetBackend - from binary_encoder import ThingSetBinaryEncoder - from client import ThingSetClient - from id import ThingSetID - from log import get_logger +from .backend import ThingSetBackend +from ..client import ThingSetClient +from ..encoders.binary import ThingSetBinaryEncoder +from ..id import ThingSetID +from ..log import get_logger logger = get_logger() + class CAN(ThingSetBackend): def __init__(self, bus: str, interface: str = "socketcan", fd=True): super().__init__() diff --git a/python_thingset/serial_backend.py b/python_thingset/backends/serial.py similarity index 88% rename from python_thingset/serial_backend.py rename to python_thingset/backends/serial.py index ff8d412..31b809d 100644 --- a/python_thingset/serial_backend.py +++ b/python_thingset/backends/serial.py @@ -8,16 +8,10 @@ from serial import Serial as PySerial -try: - from .backend import ThingSetBackend - from .client import ThingSetClient - from .log import get_logger - from .text_encoder import ThingSetTextEncoder -except ImportError: - from backend import ThingSetBackend - from client import ThingSetClient - from log import get_logger - from text_encoder import ThingSetTextEncoder +from .backend import ThingSetBackend +from ..client import ThingSetClient +from ..encoders.text import ThingSetTextEncoder +from ..log import get_logger logger = get_logger() diff --git a/python_thingset/socket_backend.py b/python_thingset/backends/socket.py similarity index 88% rename from python_thingset/socket_backend.py rename to python_thingset/backends/socket.py index d306380..12f9406 100644 --- a/python_thingset/socket_backend.py +++ b/python_thingset/backends/socket.py @@ -7,14 +7,9 @@ import socket from typing import Union -try: - from .backend import ThingSetBackend - from .binary_encoder import ThingSetBinaryEncoder - from .client import ThingSetClient -except: - from backend import ThingSetBackend - from binary_encoder import ThingSetBinaryEncoder - from client import ThingSetClient +from .backend import ThingSetBackend +from ..client import ThingSetClient +from ..encoders.binary import ThingSetBinaryEncoder class Sock(ThingSetBackend): diff --git a/python_thingset/cli.py b/python_thingset/cli.py index 4ae95cd..b06eb03 100755 --- a/python_thingset/cli.py +++ b/python_thingset/cli.py @@ -10,12 +10,8 @@ from time import sleep from typing import Union -try: - from .backend import ThingSetBackend - from .thingset import ThingSet -except ImportError: - from backend import ThingSetBackend - from thingset import ThingSet +from .backends.backend import ThingSetBackend +from .thingset import ThingSet def process_args(args: list) -> list: diff --git a/python_thingset/client.py b/python_thingset/client.py index eba33f7..688f88a 100644 --- a/python_thingset/client.py +++ b/python_thingset/client.py @@ -6,14 +6,9 @@ from abc import ABC, abstractmethod from typing import Any, List, Union -try: - from .backend import ThingSetBackend - from .response import ThingSetResponse, ThingSetStatus, ThingSetValue - from .log import get_logger -except ImportError: - from backend import ThingSetBackend - from response import ThingSetResponse, ThingSetStatus, ThingSetValue - from log import get_logger +from .backends.backend import ThingSetBackend +from .response import ThingSetResponse, ThingSetStatus, ThingSetValue +from .log import get_logger logger = get_logger() diff --git a/python_thingset/binary_encoder.py b/python_thingset/encoders/binary.py similarity index 96% rename from python_thingset/binary_encoder.py rename to python_thingset/encoders/binary.py index aa33ca5..c75c6a1 100644 --- a/python_thingset/binary_encoder.py +++ b/python_thingset/encoders/binary.py @@ -9,10 +9,7 @@ import cbor2 -try: - from .response import ThingSetRequest -except: - from response import ThingSetRequest +from ..response import ThingSetRequest class ThingSetBinaryEncoder(object): diff --git a/python_thingset/text_encoder.py b/python_thingset/encoders/text.py similarity index 100% rename from python_thingset/text_encoder.py rename to python_thingset/encoders/text.py diff --git a/python_thingset/log.py b/python_thingset/log.py index 21fae87..07960fe 100644 --- a/python_thingset/log.py +++ b/python_thingset/log.py @@ -1,4 +1,8 @@ - +# +# Copyright (c) 2024-2025 Brill Power. +# +# SPDX-License-Identifier: Apache-2.0 +# import logging import sys diff --git a/python_thingset/response.py b/python_thingset/response.py index 53d4b10..32e8f02 100644 --- a/python_thingset/response.py +++ b/python_thingset/response.py @@ -9,10 +9,7 @@ import cbor2 -try: - from .backend import ThingSetBackend -except ImportError: - from backend import ThingSetBackend +from .backends.backend import ThingSetBackend @dataclass class ThingSetStatus(object): diff --git a/python_thingset/thingset.py b/python_thingset/thingset.py index 8f3bba9..a47cd8e 100755 --- a/python_thingset/thingset.py +++ b/python_thingset/thingset.py @@ -5,18 +5,11 @@ # from typing import Any, List, Union -try: - from .backend import ThingSetBackend - from .can_backend import ThingSetCAN - from .response import ThingSetResponse - from .serial_backend import ThingSetSerial - from .socket_backend import ThingSetSock -except ImportError: - from backend import ThingSetBackend - from can_backend import ThingSetCAN - from response import ThingSetResponse - from serial_backend import ThingSetSerial - from socket_backend import ThingSetSock +from .backends.backend import ThingSetBackend +from .backends.can import ThingSetCAN +from .backends.serial import ThingSetSerial +from .backends.socket import ThingSetSock +from .response import ThingSetResponse class ThingSet(object): From 0b3c0c6678ed92a177fe42b443baea644b48bf5f Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Tue, 27 May 2025 20:32:07 +0000 Subject: [PATCH 11/16] Remove unnecessary file --- thingset | 7 ------- 1 file changed, 7 deletions(-) delete mode 100755 thingset diff --git a/thingset b/thingset deleted file mode 100755 index 65085fe..0000000 --- a/thingset +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python3 - -from python_thingset.cli import run_cli - - -if __name__ == "__main__": - run_cli() From 54caa549f7e210002d00d9758b35b2b977e8b2d2 Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Tue, 27 May 2025 21:11:14 +0000 Subject: [PATCH 12/16] ruff format --- python_thingset/backends/backend.py | 10 +- python_thingset/backends/can.py | 86 +++++++++---- python_thingset/backends/serial.py | 16 ++- python_thingset/backends/socket.py | 8 +- python_thingset/cli.py | 185 ++++++++++++++++++++-------- python_thingset/client.py | 54 ++++++-- python_thingset/encoders/binary.py | 29 +++-- python_thingset/encoders/text.py | 6 +- python_thingset/id.py | 106 +++++++++++++--- python_thingset/log.py | 1 + python_thingset/response.py | 56 +++++---- python_thingset/thingset.py | 57 ++++++--- 12 files changed, 444 insertions(+), 170 deletions(-) diff --git a/python_thingset/backends/backend.py b/python_thingset/backends/backend.py index 91ea37d..23900a9 100644 --- a/python_thingset/backends/backend.py +++ b/python_thingset/backends/backend.py @@ -11,7 +11,7 @@ class ThingSetBackend(ABC): - CAN: str = "can" + CAN: str = "can" Serial: str = "serial" Socket: str = "socket" @@ -43,20 +43,20 @@ def _handle_message(self, message: Union[bytes, can.Message]) -> None: @abstractmethod def connect(self) -> None: - """ perform backend initialisation """ + """perform backend initialisation""" pass @abstractmethod def disconnect(self) -> None: - """ perform backend teardown """ + """perform backend teardown""" pass @abstractmethod def send(self, _data: Union[bytes, can.Message]) -> None: - """ send data """ + """send data""" pass @abstractmethod def receive(self) -> Union[bytes, can.Message]: - """ receive data """ + """receive data""" pass diff --git a/python_thingset/backends/can.py b/python_thingset/backends/can.py index 2c83b42..40f2bb1 100644 --- a/python_thingset/backends/can.py +++ b/python_thingset/backends/can.py @@ -56,11 +56,11 @@ def fd(self, _fd: bool) -> None: self._fd = _fd def attach_rx_filter(self, id: int, mask: int, callback: Callable) -> None: - self._rx_filters.append({'id': id, 'mask': mask, 'callback': callback}) + self._rx_filters.append({"id": id, "mask": mask, "callback": callback}) def remove_rx_filter(self, id: int) -> None: for i, f in enumerate(self._rx_filters): - if f['id'] == id: + if f["id"] == id: self._rx_filters.pop(i) def remove_all_rx_filters(self) -> None: @@ -68,8 +68,8 @@ def remove_all_rx_filters(self) -> None: def _handle_message(self, message: can.Message) -> None: for f in self._rx_filters: - if message.arbitration_id & f['mask'] == f['id'] & f['mask']: - f['callback'](message) + if message.arbitration_id & f["mask"] == f["id"] & f["mask"]: + f["callback"](message) def connect(self) -> None: if not self._can: @@ -132,10 +132,13 @@ def tx_id(self, _id: int) -> None: self._tx_id = _id def set_address(self) -> None: - self._address = isotp.Address(addressing_mode=isotp.AddressingMode.Normal_29bits, - rxid=self.rx_id, txid=self.tx_id) + self._address = isotp.Address( + addressing_mode=isotp.AddressingMode.Normal_29bits, + rxid=self.rx_id, + txid=self.tx_id, + ) - def get_message(self, timeout: float=1.0) -> Union[bytes, None]: + def get_message(self, timeout: float = 1.0) -> Union[bytes, None]: message = None try: @@ -160,7 +163,7 @@ def disconnect(self) -> None: self._sock.close() def send(self, _data: bytes) -> None: - """ We have recursive calls to self.send here as we can't easily tell when the CAN + """We have recursive calls to self.send here as we can't easily tell when the CAN device is busy from another program (which is entirely possible) So we just retry up to 10 times - with a timeout of 100ms this equates to 1 second @@ -191,11 +194,13 @@ def receive(self) -> bytes: class ThingSetCAN(ThingSetClient, ThingSetBinaryEncoder): ADDR_CLAIM_TIMEOUT_MS: int = 500 - CONNECT_TIMEOUT_MS: int = 10000 + CONNECT_TIMEOUT_MS: int = 10000 EUI: list = [0xDE, 0xAD, 0xBE, 0xEF, 0xC0, 0xFF, 0xEE, 0xEE] - def __init__(self, bus: str, addr: int = 0x00, source_bus: int=0x00, target_bus: int=0x00): + def __init__( + self, bus: str, addr: int = 0x00, source_bus: int = 0x00, target_bus: int = 0x00 + ): super().__init__() self.backend = ThingSetBackend.CAN @@ -229,8 +234,12 @@ def _recv(self) -> bytes: def _get_isotp_ids(self, node_id: int) -> Tuple[ThingSetID]: return ( - ThingSetID.generate_req_resp_id(self.node_addr, node_id, self.source_bus, self.target_bus), - ThingSetID.generate_req_resp_id(node_id, self.node_addr, self.source_bus, self.target_bus) + ThingSetID.generate_req_resp_id( + self.node_addr, node_id, self.source_bus, self.target_bus + ), + ThingSetID.generate_req_resp_id( + node_id, self.node_addr, self.source_bus, self.target_bus + ), ) def _negotiate_address(self, desired_addr: int, timeout=5000) -> None: @@ -241,9 +250,13 @@ def _negotiate_address(self, desired_addr: int, timeout=5000) -> None: logger.debug(f"Attempting to claim node address 0x{desired_addr:02X}") - self._can.attach_rx_filter(claim_id.id, ThingSetID.ADDR_CLAIM_MASK, self._address_claim_handler) + self._can.attach_rx_filter( + claim_id.id, ThingSetID.ADDR_CLAIM_MASK, self._address_claim_handler + ) self._can.send(can.Message(arbitration_id=disco_id.id, is_fd=self._can.fd)) - self._addr_claim_timer = threading.Timer(0.5, self._address_claim_complete, args=(disco_id.target_addr,)) + self._addr_claim_timer = threading.Timer( + 0.5, self._address_claim_complete, args=(disco_id.target_addr,) + ) self._addr_claim_timer.start() def _address_claim_handler(self, message: can.Message) -> None: @@ -251,7 +264,9 @@ def _address_claim_handler(self, message: can.Message) -> None: taken_addr = ThingSetID.get_source_addr_from_id(message.arbitration_id) self._addr_claim_timer.cancel() - self._can.remove_rx_filter(message.arbitration_id & ThingSetID.ADDR_CLAIM_MASK) + self._can.remove_rx_filter( + message.arbitration_id & ThingSetID.ADDR_CLAIM_MASK + ) self._taken_node_addrs.append(taken_addr) logger.debug(f"Address 0x{taken_addr:02X} is in use by another node...") @@ -261,21 +276,46 @@ def _address_claim_handler(self, message: can.Message) -> None: self._negotiate_address(new_addr) return None - raise IOError(f"All addresses within range 0x{ThingSetID.MIN_ADDR:02X} to 0x{ThingSetID.MAX_ADDR:02X} are taken") + raise IOError( + f"All addresses within range 0x{ThingSetID.MIN_ADDR:02X} to 0x{ThingSetID.MAX_ADDR:02X} are taken" + ) else: - logger.debug(f"Device tried to claim this nodes address 0x{self.node_addr:02X}, sending claim frame") - self._can.send(can.Message(arbitration_id=ThingSetID.generate_claim_id(self.node_addr, 0x00, 0x00).id, - data=self.EUI, is_fd=self._can.fd)) + logger.debug( + f"Device tried to claim this nodes address 0x{self.node_addr:02X}, sending claim frame" + ) + self._can.send( + can.Message( + arbitration_id=ThingSetID.generate_claim_id( + self.node_addr, 0x00, 0x00 + ).id, + data=self.EUI, + is_fd=self._can.fd, + ) + ) def _address_claim_complete(self, *args: tuple) -> None: self.is_connected = True self.node_addr = args[0] self._taken_node_addrs = [] - self._can.remove_rx_filter(ThingSetID.generate_claim_id(self.node_addr, 0x00, 0x00).id & ThingSetID.ADDR_CLAIM_MASK) - self._can.attach_rx_filter(ThingSetID.generate_discovery_id(self.node_addr).id, 0xFF00FF00, self._address_claim_handler) - self._can.send(can.Message(arbitration_id=ThingSetID.generate_claim_id(self.node_addr, 0x00, 0x00).id, - data=self.EUI, is_fd=self._can.fd)) + self._can.remove_rx_filter( + ThingSetID.generate_claim_id(self.node_addr, 0x00, 0x00).id + & ThingSetID.ADDR_CLAIM_MASK + ) + self._can.attach_rx_filter( + ThingSetID.generate_discovery_id(self.node_addr).id, + 0xFF00FF00, + self._address_claim_handler, + ) + self._can.send( + can.Message( + arbitration_id=ThingSetID.generate_claim_id( + self.node_addr, 0x00, 0x00 + ).id, + data=self.EUI, + is_fd=self._can.fd, + ) + ) logger.debug(f"Claimed node address 0x{self.node_addr:02X}") diff --git a/python_thingset/backends/serial.py b/python_thingset/backends/serial.py index 31b809d..b71ae72 100644 --- a/python_thingset/backends/serial.py +++ b/python_thingset/backends/serial.py @@ -18,7 +18,7 @@ class Serial(ThingSetBackend): - def __init__(self, port: str="/dev/pts/5", baud=115200): + def __init__(self, port: str = "/dev/pts/5", baud=115200): super().__init__() self.port = port @@ -43,7 +43,7 @@ def baud(self) -> int: def baud(self, _baud) -> None: self._baud = _baud - def get_message(self, timeout: float=0.5) -> Union[str, None]: + def get_message(self, timeout: float = 0.5) -> Union[str, None]: message = None try: @@ -61,12 +61,16 @@ def _handle_message(self, message: bytes) -> None: logger.debug(decoded) - if not decoded.startswith("thingset") and not decoded.startswith("uart") and not decoded.startswith("\x1b"): + if ( + not decoded.startswith("thingset") + and not decoded.startswith("uart") + and not decoded.startswith("\x1b") + ): self._queue.put(decoded) def connect(self) -> None: if not self._serial: - self._serial = PySerial(self.port, self.baud, timeout=.1) + self._serial = PySerial(self.port, self.baud, timeout=0.1) self.start_receiving() def disconnect(self) -> None: @@ -82,7 +86,7 @@ def receive(self) -> bytes: class ThingSetSerial(ThingSetClient, ThingSetTextEncoder): - def __init__(self, port: str="/dev/pts/5", baud=115200): + def __init__(self, port: str = "/dev/pts/5", baud=115200): super().__init__() self.backend = ThingSetBackend.Serial @@ -96,7 +100,7 @@ def __init__(self, port: str="/dev/pts/5", baud=115200): def disconnect(self) -> None: self._serial.disconnect() self.is_connected = False - + def _send(self, data: bytes, _: Union[int, None]) -> None: self._serial.send(data) diff --git a/python_thingset/backends/socket.py b/python_thingset/backends/socket.py index 12f9406..ed30b39 100644 --- a/python_thingset/backends/socket.py +++ b/python_thingset/backends/socket.py @@ -27,12 +27,12 @@ def __init__(self, address: str): @property def address(self) -> str: return self._address - + @address.setter def address(self, _address) -> None: self._address = _address - def get_message(self, timeout: float=0.5) -> Union[bytes, None]: + def get_message(self, timeout: float = 0.5) -> Union[bytes, None]: message = None try: message = self._queue.get(timeout=timeout) @@ -66,7 +66,7 @@ def receive(self) -> bytes: class ThingSetSock(ThingSetClient, ThingSetBinaryEncoder): - def __init__(self, address: str="192.0.2.1"): + def __init__(self, address: str = "192.0.2.1"): super().__init__() self.backend = ThingSetBackend.Socket @@ -89,7 +89,7 @@ def _recv(self) -> bytes: @property def address(self) -> str: return self._address - + @address.setter def address(self, _address) -> None: self._address = _address diff --git a/python_thingset/cli.py b/python_thingset/cli.py index b06eb03..af2abea 100755 --- a/python_thingset/cli.py +++ b/python_thingset/cli.py @@ -36,7 +36,9 @@ def process_args(args: list) -> list: return processed_args -def get_schema(ts: ThingSet, object_id: Union[int, str], node_id: Union[int, None]=None): +def get_schema( + ts: ThingSet, object_id: Union[int, str], node_id: Union[int, None] = None +): if node_id is not None: child_ids = ts.fetch(object_id, [], node_id) @@ -49,15 +51,14 @@ def get_schema(ts: ThingSet, object_id: Union[int, str], node_id: Union[int, Non child_ids = ts.fetch("" if object_id == "00" else object_id, []) for val in child_ids.values: - for v in val.value: - print(f'{object_id if object_id != "00" else ""}/{v}') + print(f"{object_id if object_id != '00' else ''}/{v}") """ avoid shell_uart: RX ring buffer full """ sleep(0.005) if object_id != "00": - get_schema(ts, f'{object_id}/{v}') + get_schema(ts, f"{object_id}/{v}") else: get_schema(ts, v) @@ -70,45 +71,106 @@ def setup_args() -> argparse.Namespace: group = parent_parser.add_mutually_exclusive_group(required=True) """ CAN options """ - group.add_argument("-c", "--can-bus", help="Specify which CAN bus to use (example: vcan0)", - nargs="?", type=str) - parent_parser.add_argument("-t", "--target-address", help="Specify target device node address (example: 2F)") + group.add_argument( + "-c", + "--can-bus", + help="Specify which CAN bus to use (example: vcan0)", + nargs="?", + type=str, + ) + parent_parser.add_argument( + "-t", + "--target-address", + help="Specify target device node address (example: 2F)", + ) """ Serial options """ - group.add_argument("-p", "--port", help="Specify which serial port to use (example: /dev/pts/5)", - nargs="?", type=str) - parent_parser.add_argument("-r", "--baud-rate", help="Specify serial baud rate (example: 115200)", - nargs="?", default=115200, type=int) - + group.add_argument( + "-p", + "--port", + help="Specify which serial port to use (example: /dev/pts/5)", + nargs="?", + type=str, + ) + parent_parser.add_argument( + "-r", + "--baud-rate", + help="Specify serial baud rate (example: 115200)", + nargs="?", + default=115200, + type=int, + ) + """ Socket options """ - group.add_argument("-i", "--ip", help="Specify which IPv4 address to connect to (example 192.0.2.1)") + group.add_argument( + "-i", + "--ip", + help="Specify which IPv4 address to connect to (example 192.0.2.1)", + ) """ Functions """ - subparsers = arg_parser.add_subparsers(dest="method", required=True, help="ThingSet function execute " \ - "(one of: exec, fetch, get, update, schema)") - - get_parser = subparsers.add_parser("get", help="Perform ThingSet get request", parents=[parent_parser]) - get_parser.add_argument("id", help="Path or ID of value to retreive (example Build/rBoard, or F03)") - - fetch_parser = subparsers.add_parser("fetch", parents=[parent_parser], help="Perform ThingSet fetch request") - fetch_parser.add_argument("parent_id", help="Path or ID for parent node of value(s) to retrieve (example: Build)") - fetch_parser.add_argument("value_ids", help="Paths or IDs (space delimited) for values to retrieve (example: rBoard " \ - "rBuildUser or F03 F02 or can be empty)", nargs="*") - - exec_parser = subparsers.add_parser("exec", parents=[parent_parser], help="Perform ThingSet exec request") - exec_parser.add_argument("value_id", help="Path or ID of function to execute (example: Module/xSaveNVM or 5F)") - exec_parser.add_argument("values", help="Arguments to function (space delimited) (example: let-me-in or 24.6 " \ - "or can be empty) (numeric values should be decimal)", nargs="*") - - update_parser = subparsers.add_parser("update", parents=[parent_parser], help="Perform ThingSet update request") - update_parser.add_argument("update_args", help="If using -p/--port: path value - Path of value to update (example: " \ - "Module/sCanMaxLogLevel 3) (value is decimal if numeric). If using -c/--can-bus: " \ - "parent_id value_id value - (example: 0F F02 MyValue)", nargs="*") - - schema_parser = subparsers.add_parser("schema", parents=[parent_parser], help="Get ThingSet schema for device") - schema_parser.add_argument("root_id", help="Path or ID of node at which to start schema fetch (example: Module or 0F) " \ - "(\"\" or 00 for root path) (leave empty to fetch full schema)", - nargs="?", default="00") + subparsers = arg_parser.add_subparsers( + dest="method", + required=True, + help="ThingSet function execute (one of: exec, fetch, get, update, schema)", + ) + + get_parser = subparsers.add_parser( + "get", help="Perform ThingSet get request", parents=[parent_parser] + ) + get_parser.add_argument( + "id", help="Path or ID of value to retreive (example Build/rBoard, or F03)" + ) + + fetch_parser = subparsers.add_parser( + "fetch", parents=[parent_parser], help="Perform ThingSet fetch request" + ) + fetch_parser.add_argument( + "parent_id", + help="Path or ID for parent node of value(s) to retrieve (example: Build)", + ) + fetch_parser.add_argument( + "value_ids", + help="Paths or IDs (space delimited) for values to retrieve (example: rBoard " + "rBuildUser or F03 F02 or can be empty)", + nargs="*", + ) + + exec_parser = subparsers.add_parser( + "exec", parents=[parent_parser], help="Perform ThingSet exec request" + ) + exec_parser.add_argument( + "value_id", + help="Path or ID of function to execute (example: Module/xSaveNVM or 5F)", + ) + exec_parser.add_argument( + "values", + help="Arguments to function (space delimited) (example: let-me-in or 24.6 " + "or can be empty) (numeric values should be decimal)", + nargs="*", + ) + + update_parser = subparsers.add_parser( + "update", parents=[parent_parser], help="Perform ThingSet update request" + ) + update_parser.add_argument( + "update_args", + help="If using -p/--port: path value - Path of value to update (example: " + "Module/sCanMaxLogLevel 3) (value is decimal if numeric). If using -c/--can-bus: " + "parent_id value_id value - (example: 0F F02 MyValue)", + nargs="*", + ) + + schema_parser = subparsers.add_parser( + "schema", parents=[parent_parser], help="Get ThingSet schema for device" + ) + schema_parser.add_argument( + "root_id", + help="Path or ID of node at which to start schema fetch (example: Module or 0F) " + '("" or 00 for root path) (leave empty to fetch full schema)', + nargs="?", + default="00", + ) args = arg_parser.parse_args() @@ -119,8 +181,10 @@ def setup_args() -> argparse.Namespace: if args.method == "update": if len(args.update_args) != 3: - arg_parser.error("When using update with -c/--can-bus you must suply a parent_id, value_id and value " \ - "(example: thingset update f f03 MyValue -c vcan0") + arg_parser.error( + "When using update with -c/--can-bus you must suply a parent_id, value_id and value " + "(example: thingset update f f03 MyValue -c vcan0" + ) else: args.parent_id = args.update_args[0] args.value_id = args.update_args[1] @@ -130,8 +194,10 @@ def setup_args() -> argparse.Namespace: elif args.port: if args.method == "update": if len(args.update_args) != 2: - arg_parser.error("When using update with -p/--port you must suply a path and a value (example: " \ - "thingset update Module/sCanMaxLogLevel 4 -p /dev/pts/5") + arg_parser.error( + "When using update with -p/--port you must suply a path and a value (example: " + "thingset update Module/sCanMaxLogLevel 4 -p /dev/pts/5" + ) else: args.parent_id = args.update_args[0] args.value = [args.update_args[1]] @@ -142,8 +208,10 @@ def setup_args() -> argparse.Namespace: if args.method == "update": if len(args.update_args) != 3: - arg_parser.error("When using update with -i/--ip you must suply a parent_id, value_id and value " \ - "(example: thingset update f f03 MyValue -i 192.0.2.1") + arg_parser.error( + "When using update with -i/--ip you must suply a parent_id, value_id and value " + "(example: thingset update f f03 MyValue -i 192.0.2.1" + ) else: args.parent_id = args.update_args[0] args.value_id = args.update_args[1] @@ -173,9 +241,15 @@ def run_cli(): if args.backend.lower() == "serial": response = ts.fetch(args.parent_id, args.value_ids) elif args.backend.lower() == "socket": - response = ts.fetch(int(args.parent_id, 16), [int(i, 16) for i in args.value_ids]) + response = ts.fetch( + int(args.parent_id, 16), [int(i, 16) for i in args.value_ids] + ) else: - response = ts.fetch(int(args.parent_id, 16), [int(i, 16) for i in args.value_ids], int(args.target_address, 16)) + response = ts.fetch( + int(args.parent_id, 16), + [int(i, 16) for i in args.value_ids], + int(args.target_address, 16), + ) case "exec": p_args = process_args(args.values) @@ -184,16 +258,29 @@ def run_cli(): elif args.backend.lower() == "socket": response = ts.exec(int(args.value_id, 16), p_args) else: - response = ts.exec(int(args.value_id, 16), p_args, node_id=int(args.target_address, 16)) + response = ts.exec( + int(args.value_id, 16), + p_args, + node_id=int(args.target_address, 16), + ) case "update": if args.backend.lower() == "serial": response = ts.update(args.parent_id, args.value) elif args.backend.lower() == "socket": p_args = process_args(args.value) - response = ts.update(int(args.value_id, 16), p_args[0], parent_id=int(args.parent_id, 16)) + response = ts.update( + int(args.value_id, 16), + p_args[0], + parent_id=int(args.parent_id, 16), + ) else: p_args = process_args(args.value) - response = ts.update(int(args.value_id, 16), p_args[0], int(args.target_address, 16), int(args.parent_id, 16)) + response = ts.update( + int(args.value_id, 16), + p_args[0], + int(args.target_address, 16), + int(args.parent_id, 16), + ) case "schema": if args.backend.lower() == "serial": get_schema(ts, args.root_id) diff --git a/python_thingset/client.py b/python_thingset/client.py index 688f88a..553ad5a 100644 --- a/python_thingset/client.py +++ b/python_thingset/client.py @@ -15,7 +15,13 @@ class ThingSetClient(ABC): - def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: Union[int, None]=None, get_paths: bool=True) -> ThingSetResponse: + def fetch( + self, + parent_id: Union[int, str], + ids: List[Union[int, str]], + node_id: Union[int, None] = None, + get_paths: bool = True, + ) -> ThingSetResponse: values = [] self._send(self.encode_fetch(parent_id, ids), node_id) @@ -29,19 +35,30 @@ def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: if self.backend == ThingSetBackend.Serial: values.append(ThingSetValue(None, tmp.data, parent_id)) else: - values.append(self._create_value(parent_id, node_id, tmp.data, get_paths)) + values.append( + self._create_value(parent_id, node_id, tmp.data, get_paths) + ) else: for idx, id in enumerate(ids): if self.backend == ThingSetBackend.Serial: values.append(ThingSetValue(None, tmp.data[idx], id)) else: - values.append(self._create_value(id, node_id, tmp.data[idx], get_paths)) + values.append( + self._create_value( + id, node_id, tmp.data[idx], get_paths + ) + ) return ThingSetResponse(self.backend, msg, values) - def get(self, value_id: Union[int, str], node_id: Union[int, None]=None, get_paths: bool=True) -> ThingSetResponse: + def get( + self, + value_id: Union[int, str], + node_id: Union[int, None] = None, + get_paths: bool = True, + ) -> ThingSetResponse: values = [] - + self._send(self.encode_get(value_id), node_id) msg = self._recv() @@ -52,19 +69,34 @@ def get(self, value_id: Union[int, str], node_id: Union[int, None]=None, get_pat if self.backend == ThingSetBackend.Serial: values.append(ThingSetValue(None, tmp.data, value_id)) else: - values.append(self._create_value(value_id, node_id, tmp.data, get_paths)) + values.append( + self._create_value(value_id, node_id, tmp.data, get_paths) + ) return ThingSetResponse(self.backend, msg, values) - def update(self, value_id: Union[int, str], value: Any, node_id: Union[int, None]=None, parent_id: Union[int, None]=None) -> ThingSetResponse: + def update( + self, + value_id: Union[int, str], + value: Any, + node_id: Union[int, None] = None, + parent_id: Union[int, None] = None, + ) -> ThingSetResponse: self._send(self.encode_update(parent_id, value_id, value), node_id) return ThingSetResponse(self.backend, self._recv()) - def exec(self, value_id: Union[int, str], args: Union[List[Any], None], node_id: Union[int, None]=None) -> ThingSetResponse: + def exec( + self, + value_id: Union[int, str], + args: Union[List[Any], None], + node_id: Union[int, None] = None, + ) -> ThingSetResponse: self._send(self.encode_exec(value_id, args), node_id) return ThingSetResponse(self.backend, self._recv()) - def _create_value(self, value_id: int, node_id: int, value: Any, get_paths: bool) -> ThingSetValue: + def _create_value( + self, value_id: int, node_id: int, value: Any, get_paths: bool + ) -> ThingSetValue: path = None if get_paths: @@ -78,7 +110,7 @@ def _create_value(self, value_id: int, node_id: int, value: Any, get_paths: bool path = tmp.data[0] else: logger.warning("Failed to read value path") - + return ThingSetValue(value_id, value, path) @abstractmethod @@ -104,7 +136,7 @@ def is_connected(self, _is_connected: bool) -> None: @property def backend(self) -> str: return self._backend - + @backend.setter def backend(self, _backend) -> None: self._backend = _backend diff --git a/python_thingset/encoders/binary.py b/python_thingset/encoders/binary.py index c75c6a1..3abe3ff 100644 --- a/python_thingset/encoders/binary.py +++ b/python_thingset/encoders/binary.py @@ -13,7 +13,7 @@ class ThingSetBinaryEncoder(object): - PATHS = 0x17 + PATHS = 0x17 NULL_BYTE = 0xF6 def __init__(self): @@ -27,21 +27,22 @@ def __init__(self): 18 40 # CBOR uint: 0x40 (object ID) 18 41 # CBOR uint: 0x41 (object ID) """ + def encode_fetch(self, parent_id: int, value_ids: List[int]) -> bytes: req = bytearray() req.append(ThingSetRequest.FETCH) req += cbor2.dumps(parent_id, canonical=True) - if (len(value_ids) == 0): + if len(value_ids) == 0: req.append(self.NULL_BYTE) else: req += cbor2.dumps(value_ids, canonical=True) return req - + def encode_get(self, value_id: int) -> bytes: return bytes([ThingSetRequest.GET] + list(cbor2.dumps(value_id))) - + def encode_exec(self, value_id: int, args: Union[Any, None]) -> bytes: p_args = list() @@ -55,8 +56,12 @@ def encode_exec(self, value_id: int, args: Union[Any, None]) -> bytes: p_args.append(a) else: p_args.append(a) - - return bytes([ThingSetRequest.EXEC] + list(cbor2.dumps(value_id)) + list(cbor2.dumps(p_args, canonical=True))) + + return bytes( + [ThingSetRequest.EXEC] + + list(cbor2.dumps(value_id)) + + list(cbor2.dumps(p_args, canonical=True)) + ) def encode_update(self, parent_id: int, value_id: int, value: Any) -> bytes: if isinstance(value, float): @@ -65,16 +70,20 @@ def encode_update(self, parent_id: int, value_id: int, value: Any) -> bytes: if value.lower() == "true" or value.lower() == "false": value = json.loads(value.lower()) - return bytes([ThingSetRequest.UPDATE] + list(cbor2.dumps(parent_id)) + list(cbor2.dumps({value_id:value}, canonical=True))) + return bytes( + [ThingSetRequest.UPDATE] + + list(cbor2.dumps(parent_id)) + + list(cbor2.dumps({value_id: value}, canonical=True)) + ) def encode_get_path(self, value_id: int) -> bytes: req = bytearray([ThingSetRequest.FETCH, self.PATHS]) req.extend(cbor2.dumps([value_id])) - + return req def to_f32(self, value: float) -> float: - """ In Python, all floats are actually doubles. This does not map well to embedded targets where + """In Python, all floats are actually doubles. This does not map well to embedded targets where there is a clear distinction between the two. This function forces the provided floating point argument, value, to its closest 32-bit @@ -82,4 +91,4 @@ def to_f32(self, value: float) -> float: and can be properly parsed by ThingSet running on an embedded target when expecting a float """ - return struct.unpack('f', struct.pack('f', value))[0] + return struct.unpack("f", struct.pack("f", value))[0] diff --git a/python_thingset/encoders/text.py b/python_thingset/encoders/text.py index d38e689..2f7188c 100644 --- a/python_thingset/encoders/text.py +++ b/python_thingset/encoders/text.py @@ -27,7 +27,7 @@ def encode_get(self, value_id: str) -> bytes: return f"thingset ?{value_id}\n".encode() def encode_exec(self, value_id: str, args: Union[Any, None]) -> bytes: - """ properly format strings for transmission, add args to stringified list """ + """properly format strings for transmission, add args to stringified list""" processed_args = "[" """ leave numeric values as is, surround strings with escape chars """ @@ -51,9 +51,9 @@ def encode_exec(self, value_id: str, args: Union[Any, None]) -> bytes: processed_args += "]" return f"""thingset !{value_id} {processed_args}\n""".encode() - + def encode_update(self, parent_id: None, value_id: str, value: Any) -> bytes: - """ properly format strings for transmission, add args to stringified list """ + """properly format strings for transmission, add args to stringified list""" value = value[0] val = None diff --git a/python_thingset/id.py b/python_thingset/id.py index dd55e05..cc54861 100644 --- a/python_thingset/id.py +++ b/python_thingset/id.py @@ -20,9 +20,9 @@ class ThingSetIDType(object): class ThingSetIDPriority(object): NET_MGMT: int = 0x04 << 26 PUB_HIGH: int = 0x05 << 26 - SERVICE: int = 0x06 << 26 + SERVICE: int = 0x06 << 26 REQ_RESP: int = SERVICE - PUB_LOW: int = 0x07 << 26 + PUB_LOW: int = 0x07 << 26 class ThingSetID(object): @@ -30,16 +30,24 @@ class ThingSetID(object): MAX_ADDR: int = 0xFD SRC_ADDR_GATEWAY_DEFAULT = MIN_ADDR - SRC_ADDR_ANON: int = 0xFE - SRC_ADDR_BCAST: int = 0xFF + SRC_ADDR_ANON: int = 0xFE + SRC_ADDR_BCAST: int = 0xFF TYPE_DISCOVERY: int = 0x01 - TYPE_CLAIM: int = 0x02 - TYPE_REQ_RESP: int = 0x03 + TYPE_CLAIM: int = 0x02 + TYPE_REQ_RESP: int = 0x03 ADDR_CLAIM_MASK: int = 0xFF00FFFF - def __init__(self, source_addr: int, target_addr: int, priority: ThingSetIDPriority, type: ThingSetIDType, source_bus: Union[int, None]=None, target_bus: Union[int, None]=None): + def __init__( + self, + source_addr: int, + target_addr: int, + priority: ThingSetIDPriority, + type: ThingSetIDType, + source_bus: Union[int, None] = None, + target_bus: Union[int, None] = None, + ): self.source_addr = source_addr self.target_addr = target_addr self.priority = priority @@ -53,24 +61,58 @@ def __init__(self, source_addr: int, target_addr: int, priority: ThingSetIDPrior Else if target address is 0xFF and priority and type are network management, then this is a claim frame identifier """ - if self.source_addr == self.SRC_ADDR_ANON and self.priority == ThingSetIDPriority.NET_MGMT and self.type == ThingSetIDType.NET_MGMT: + if ( + self.source_addr == self.SRC_ADDR_ANON + and self.priority == ThingSetIDPriority.NET_MGMT + and self.type == ThingSetIDType.NET_MGMT + ): self.id = self.TYPE_DISCOVERY - elif self.target_addr == self.SRC_ADDR_BCAST and self.priority == ThingSetIDPriority.NET_MGMT and self.type == ThingSetIDType.NET_MGMT: + elif ( + self.target_addr == self.SRC_ADDR_BCAST + and self.priority == ThingSetIDPriority.NET_MGMT + and self.type == ThingSetIDType.NET_MGMT + ): self.id = self.TYPE_CLAIM - elif self.priority == ThingSetIDPriority.REQ_RESP and type == ThingSetIDType.REQ_RESP: + elif ( + self.priority == ThingSetIDPriority.REQ_RESP + and type == ThingSetIDType.REQ_RESP + ): self.id = self.TYPE_REQ_RESP @classmethod def generate_discovery_id(cls, target_addr) -> "ThingSetID": - return ThingSetID(cls.SRC_ADDR_ANON, target_addr, ThingSetIDPriority.NET_MGMT, ThingSetIDType.NET_MGMT) + return ThingSetID( + cls.SRC_ADDR_ANON, + target_addr, + ThingSetIDPriority.NET_MGMT, + ThingSetIDType.NET_MGMT, + ) @classmethod - def generate_claim_id(cls, source_addr: int, source_bus: int, target_bus: int) -> "ThingSetID": - return ThingSetID(source_addr, cls.SRC_ADDR_BCAST, ThingSetIDPriority.NET_MGMT, ThingSetIDType.NET_MGMT, source_bus=source_bus, target_bus=target_bus) + def generate_claim_id( + cls, source_addr: int, source_bus: int, target_bus: int + ) -> "ThingSetID": + return ThingSetID( + source_addr, + cls.SRC_ADDR_BCAST, + ThingSetIDPriority.NET_MGMT, + ThingSetIDType.NET_MGMT, + source_bus=source_bus, + target_bus=target_bus, + ) @classmethod - def generate_req_resp_id(cls, source_addr: int, target_addr: int, source_bus: int, target_bus: int) -> "ThingSetID": - return ThingSetID(source_addr, target_addr, ThingSetIDPriority.REQ_RESP, ThingSetIDType.REQ_RESP, source_bus=source_bus, target_bus=target_bus) + def generate_req_resp_id( + cls, source_addr: int, target_addr: int, source_bus: int, target_bus: int + ) -> "ThingSetID": + return ThingSetID( + source_addr, + target_addr, + ThingSetIDPriority.REQ_RESP, + ThingSetIDType.REQ_RESP, + source_bus=source_bus, + target_bus=target_bus, + ) @staticmethod def get_source_addr_from_id(id: int) -> int: @@ -88,11 +130,31 @@ def id(self) -> int: def id(self, _id_type) -> None: match _id_type: case self.TYPE_DISCOVERY: - self._id = self.priority | self.type | randint(0x00, 0xFE) << 16 | self.target_addr << 8 | self.SRC_ADDR_ANON + self._id = ( + self.priority + | self.type + | randint(0x00, 0xFE) << 16 + | self.target_addr << 8 + | self.SRC_ADDR_ANON + ) case self.TYPE_CLAIM: - self._id = self.priority | self.type | self.target_bus << 20 | self.source_bus << 16 | self.SRC_ADDR_BCAST << 8 | self.source_addr + self._id = ( + self.priority + | self.type + | self.target_bus << 20 + | self.source_bus << 16 + | self.SRC_ADDR_BCAST << 8 + | self.source_addr + ) case self.TYPE_REQ_RESP: - self._id = self.priority | self.type | self.target_bus << 20 | self.source_bus << 16 | self.target_addr << 8 | self.source_addr + self._id = ( + self.priority + | self.type + | self.target_bus << 20 + | self.source_bus << 16 + | self.target_addr << 8 + | self.source_addr + ) case _: self._id = None raise ValueError(f"Unknown ID type ({hex(_id_type)})") @@ -104,7 +166,9 @@ def source_addr(self) -> int: @source_addr.setter def source_addr(self, _addr: int) -> None: if _addr < self.MIN_ADDR or _addr > self.SRC_ADDR_BCAST: - raise ValueError(f"Source address ({hex(_addr)}) must be between {self.MIN_ADDR} and {self.SRC_ADDR_BCAST} inclusive") + raise ValueError( + f"Source address ({hex(_addr)}) must be between {self.MIN_ADDR} and {self.SRC_ADDR_BCAST} inclusive" + ) self._source_addr = _addr @@ -115,7 +179,9 @@ def target_addr(self) -> int: @target_addr.setter def target_addr(self, _addr: int) -> None: if _addr < self.MIN_ADDR or _addr > self.SRC_ADDR_BCAST: - raise ValueError(f"Target address ({hex(_addr)}) must be between {self.MIN_ADDR} and {self.SRC_ADDR_BCAST} inclusive") + raise ValueError( + f"Target address ({hex(_addr)}) must be between {self.MIN_ADDR} and {self.SRC_ADDR_BCAST} inclusive" + ) self._target_addr = _addr diff --git a/python_thingset/log.py b/python_thingset/log.py index 07960fe..a9e1d60 100644 --- a/python_thingset/log.py +++ b/python_thingset/log.py @@ -6,6 +6,7 @@ import logging import sys + def get_logger() -> logging.Logger: logger = logging.getLogger(__name__) if not logger.hasHandlers(): diff --git a/python_thingset/response.py b/python_thingset/response.py index 32e8f02..5b32b6b 100644 --- a/python_thingset/response.py +++ b/python_thingset/response.py @@ -11,33 +11,34 @@ from .backends.backend import ThingSetBackend + @dataclass class ThingSetStatus(object): - """ Dataclass to contain ThingSet status codes + """Dataclass to contain ThingSet status codes and their names plus utility functions """ - CREATED: int = 0x81 - DELETED: int = 0x82 - CHANGED: int = 0x84 - CONTENT: int = 0x85 - BAD_REQUEST: int = 0xA0 - UNAUTHORISED: int = 0xA1 - FORBIDDEN: int = 0xA3 - NOT_FOUND: int = 0xA4 - NOT_ALLOWED: int = 0xA5 + CREATED: int = 0x81 + DELETED: int = 0x82 + CHANGED: int = 0x84 + CONTENT: int = 0x85 + BAD_REQUEST: int = 0xA0 + UNAUTHORISED: int = 0xA1 + FORBIDDEN: int = 0xA3 + NOT_FOUND: int = 0xA4 + NOT_ALLOWED: int = 0xA5 REQUEST_INCOMPLETE: int = 0xA8 - CONFLICT: int = 0xA9 - REQUEST_TOO_LARGE: int = 0xAD + CONFLICT: int = 0xA9 + REQUEST_TOO_LARGE: int = 0xAD UNSUPPORTED_FORMAT: int = 0xAF - INTERNAL_ERROR: int = 0xC0 - NOT_IMPLEMENTED: int = 0xC1 - GATEWAY_TIMEOUT: int = 0xC4 - NOT_GATEWAY: int = 0xC5 + INTERNAL_ERROR: int = 0xC0 + NOT_IMPLEMENTED: int = 0xC1 + GATEWAY_TIMEOUT: int = 0xC4 + NOT_GATEWAY: int = 0xC5 @staticmethod def status_code_name(code: int) -> Union[str, None]: - """ Get status code name from status code integer + """Get status code name from status code integer Args: code: an integer corresponding to a `ThingSetStatus` attribute @@ -55,20 +56,20 @@ def status_code_name(code: int) -> Union[str, None]: @dataclass class ThingSetRequest(object): - """ Dataclass to contain ThingSet request codes + """Dataclass to contain ThingSet request codes and their names plus utility functions """ - GET: int = 0x01 - EXEC: int = 0x02 + GET: int = 0x01 + EXEC: int = 0x02 DELETE: int = 0x04 - FETCH: int = 0x05 + FETCH: int = 0x05 CREATE: int = 0x06 UPDATE: int = 0x07 @staticmethod def request_name(req: int) -> Union[str, None]: - """ Get request name from request integer + """Get request name from request integer Args: req: an integer corresponding to a `ThingSetRequest` attribute @@ -154,8 +155,13 @@ class ThingSetResponse(object): MODE_BIN = 0x1 MODE_TXT = 0x2 - def __init__(self, backend: str, data: Union[bytes, str, None], values: Union[List[ThingSetValue], None] = None): - """ mode is set based on type of backend; binary for CAN or sockets or text for serial """ + def __init__( + self, + backend: str, + data: Union[bytes, str, None], + values: Union[List[ThingSetValue], None] = None, + ): + """mode is set based on type of backend; binary for CAN or sockets or text for serial""" self.mode = backend self.status_code = None @@ -217,7 +223,7 @@ def _get_status_byte(self, data: bytes) -> int: return None def _strip_null(self, data: bytes) -> bytes: - return data[1:].replace(b'\xf6', b'') + return data[1:].replace(b"\xf6", b"") @property def mode(self) -> int: diff --git a/python_thingset/thingset.py b/python_thingset/thingset.py index a47cd8e..bde0cc8 100755 --- a/python_thingset/thingset.py +++ b/python_thingset/thingset.py @@ -13,10 +13,19 @@ class ThingSet(object): - def __init__(self, backend: str="can", can_bus: str="vcan0", can_addr: int=0x00, init_block: bool=True, - source_bus: int=0x00, target_bus: int=0x00, port: str="/dev/pts/5", baud: int=115200, - ip_addr: str="192.0.2.1") -> "ThingSet": - """ Constructor for ThingSet object + def __init__( + self, + backend: str = "can", + can_bus: str = "vcan0", + can_addr: int = 0x00, + init_block: bool = True, + source_bus: int = 0x00, + target_bus: int = 0x00, + port: str = "/dev/pts/5", + baud: int = 115200, + ip_addr: str = "192.0.2.1", + ) -> "ThingSet": + """Constructor for ThingSet object Args: backend: communications backend to use - one of `'can'` or `'serial'` @@ -37,7 +46,9 @@ def __init__(self, backend: str="can", can_bus: str="vcan0", can_addr: int=0x00, match backend.lower(): case ThingSetBackend.CAN: - self.backend = ThingSetCAN(can_bus, can_addr, source_bus=source_bus, target_bus=target_bus) + self.backend = ThingSetCAN( + can_bus, can_addr, source_bus=source_bus, target_bus=target_bus + ) case ThingSetBackend.Serial: self.backend = ThingSetSerial(port, baud) case ThingSetBackend.Socket: @@ -52,7 +63,7 @@ def __init__(self, backend: str="can", can_bus: str="vcan0", can_addr: int=0x00, pass def disconnect(self) -> None: - """ Initiate disconnection from communications backend + """Initiate disconnection from communications backend Args: None @@ -64,8 +75,13 @@ def disconnect(self) -> None: if self.backend is not None: return self.backend.disconnect() - def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: Union[int, None]=None) -> ThingSetResponse: - """ Perform a ThingSet fetch request + def fetch( + self, + parent_id: Union[int, str], + ids: List[Union[int, str]], + node_id: Union[int, None] = None, + ) -> ThingSetResponse: + """Perform a ThingSet fetch request Args: parent_id: id of (CAN), or path to (serial), parent group @@ -79,8 +95,10 @@ def fetch(self, parent_id: Union[int, str], ids: List[Union[int, str]], node_id: if self.backend is not None: return self.backend.fetch(parent_id, ids, node_id) - def get(self, value_id: Union[int, str], node_id: Union[int, None]=None) -> ThingSetResponse: - """ Perform a ThingSet get request + def get( + self, value_id: Union[int, str], node_id: Union[int, None] = None + ) -> ThingSetResponse: + """Perform a ThingSet get request Args: value_id: id of (CAN), or path to (serial), value to retrieve @@ -93,8 +111,13 @@ def get(self, value_id: Union[int, str], node_id: Union[int, None]=None) -> Thin if self.backend is not None: return self.backend.get(value_id, node_id) - def exec(self, value_id: Union[int, str], args: Union[List[Any], None], node_id: Union[int, None]=None) -> ThingSetResponse: - """ Perform a ThingSet exec request + def exec( + self, + value_id: Union[int, str], + args: Union[List[Any], None], + node_id: Union[int, None] = None, + ) -> ThingSetResponse: + """Perform a ThingSet exec request Args: value_id: id of (CAN), or path to (serial), function to execute @@ -108,8 +131,14 @@ def exec(self, value_id: Union[int, str], args: Union[List[Any], None], node_id: if self.backend is not None: return self.backend.exec(value_id, args, node_id) - def update(self, value_id: Union[int, str], value: Any, node_id: Union[int, None]=None, parent_id: Union[int, None]=None) -> ThingSetResponse: - """ Perform a ThingSet update request + def update( + self, + value_id: Union[int, str], + value: Any, + node_id: Union[int, None] = None, + parent_id: Union[int, None] = None, + ) -> ThingSetResponse: + """Perform a ThingSet update request Args: value_id: id of (CAN), or path to (serial), value to update From 0a076b3b3973e5c018315d46fee18d35c06f394c Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Tue, 27 May 2025 21:16:15 +0000 Subject: [PATCH 13/16] Maybe run ruff in CI? --- .github/workflows/lint.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..4adeffd --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,8 @@ +name: Ruff +on: [ push, pull_request ] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v3 From 01c794022b3e01f88865f99e9c941608c74dfcd6 Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Tue, 27 May 2025 21:18:40 +0000 Subject: [PATCH 14/16] Fix linter issue --- python_thingset/backends/can.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_thingset/backends/can.py b/python_thingset/backends/can.py index 40f2bb1..af45b53 100644 --- a/python_thingset/backends/can.py +++ b/python_thingset/backends/can.py @@ -180,7 +180,7 @@ def send(self, _data: bytes) -> None: self._send_recurse_ctr += 1 if self._send_recurse_ctr >= 10: self._send_recurse_ctr = 0 - logger.error(f"ISOTP transmission retry limit exceeded") + logger.error("ISOTP transmission retry limit exceeded") return None self.send(_data) From cdebcb5010533938c4b0ea0dc7b354bded393f0c Mon Sep 17 00:00:00 2001 From: Gareth Potter Date: Wed, 28 May 2025 08:16:25 +0100 Subject: [PATCH 15/16] fix requirements.txt suggestion in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 61a8c68..ee0d3fe 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ Simply include in your `requirements.txt` (or equivalent file) as: ``` -python_thingset @ git+ssh://git@github.com:Brill-Power/python-thingset.git +python_thingset @ git+https://github.com/Brill-Power/python-thingset.git ``` If you wish to work from a specific branch, for example a branch called `fix-package-imports`, append `@fix-package-imports` to the above line in `requirements.txt`, as follows: ``` -python_thingset @ git+ssh://git@github.com:Brill-Power/python-thingset.git@fix-package-imports +python_thingset @ git+https://github.com/Brill-Power/python-thingset.git@fix-package-imports ``` #### To get a value: From d032df45d805faf226696ead43b4b8dde47f5433 Mon Sep 17 00:00:00 2001 From: Gareth Potter Date: Wed, 28 May 2025 09:04:11 +0100 Subject: [PATCH 16/16] obey markdown linter somewhat --- README.md | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ee0d3fe..8a263e4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## To use from Python -#### To install: +### To install Simply include in your `requirements.txt` (or equivalent file) as: @@ -16,8 +16,9 @@ If you wish to work from a specific branch, for example a branch called `fix-pac python_thingset @ git+https://github.com/Brill-Power/python-thingset.git@fix-package-imports ``` -#### To get a value: -``` +### To get a value + +```python from python_thingset.thingset import ThingSet with ThingSet() as ts: @@ -34,8 +35,9 @@ with ThingSet() as ts: print(v.name, f"0x{v.id:02X}", v.value) ``` -#### To fetch multiple values: -``` +### To fetch multiple values + +```python from python_thingset.thingset import ThingSet with ThingSet() as ts: @@ -52,8 +54,9 @@ with ThingSet() as ts: print(v.name, f"0x{v.id:02X}", v.value) ``` -#### To fetch all child IDs of a parent: -``` +### To fetch all child IDs of a parent + +```python from python_thingset.thingset import ThingSet with ThingSet() as ts: @@ -71,8 +74,9 @@ with ThingSet() as ts: print(v.name, f"0x{v.id:02X}", [f"0x{i:02X}" for i in v.value]) ``` -#### To execute a function: -``` +### To execute a function + +```python from python_thingset.thingset import ThingSet with ThingSet() as ts: @@ -85,8 +89,9 @@ with ThingSet() as ts: print(response.data) ``` -#### To update a value: -``` +### To update a value + +```python from python_thingset.thingset import ThingSet with ThingSet() as ts: @@ -101,9 +106,9 @@ with ThingSet() as ts: ## To use from terminal -#### To install: +### To install -``` +```bash 1. git clone git@github.com:Brill-Power/python-thingset.git 2. cd python_thingset 3. pip install -r requirements.txt @@ -113,7 +118,7 @@ with ThingSet() as ts: This will clone the latest version of the repository, make the file `thingset` executable and then add the directory containing the file `thingset` to your `PATH` such that it will be executable from any directory. -#### Serial examples: +### Serial examples ``` thingset get SomeGroup -p /dev/pts/5 @@ -134,7 +139,7 @@ thingset schema SomeGroup -p /dev/pts/5 thingset schema "" -p /dev/pts/5 ``` -#### CAN examples: +### CAN examples ``` thingset get f -c vcan0 -t 2f @@ -153,9 +158,9 @@ thingset schema -c vcan0 -t 2f thingset schema f -c vcan0 -t 2f ``` -#### Socket examples +### Socket examples -``` +```python if __name__ == "__main__": s = ThingSetSock("192.0.2.1")