diff --git a/scripts/test-ae33.py b/scripts/test-ae33.py index ce647273c3941252327315cca17a30cd2e832bd2..7429dbf840854b50eedc90a895e2c6079b815530 100644 --- a/scripts/test-ae33.py +++ b/scripts/test-ae33.py @@ -6,22 +6,22 @@ import time from umnp.communication.serial_connection import SerialConnection from umnp.devices.aethalometer.ae33 import AE33 -now = datetime.datetime.now() -if now.microsecond >= 500 * 1000: - now = now + datetime.timedelta(seconds=1) -now = now.replace(microsecond=0) -now_string = now.isoformat().replace(":", "-") -logger = logging.getLogger("ae33-test") -logger.setLevel(logging.DEBUG) -log_file_fn = os.path.join("logs", f"ae33-{now_string}.log") -log_file = logging.FileHandler(log_file_fn) -log_file.setLevel(logging.DEBUG) -logger.addHandler(log_file) +# now = datetime.datetime.now() +# if now.microsecond >= 500 * 1000: +# now = now + datetime.timedelta(seconds=1) +# now = now.replace(microsecond=0) +# now_string = now.isoformat().replace(":", "-") +# logger = logging.getLogger("ae33-test") +# logger.setLevel(logging.DEBUG) +# log_file_fn = os.path.join("logs", f"ae33-{now_string}.log") +# log_file = logging.FileHandler(log_file_fn) +# log_file.setLevel(logging.DEBUG) +# logger.addHandler(log_file) def main(): conn_opts = {"baudrate": 115200, "dsrdtr": True, "rtscts": False, "xonoff": False} - connection = SerialConnection("/dev/ttyUSB2", options=conn_opts) + connection = SerialConnection("/dev/ttyUSB0", options=conn_opts) ae33 = AE33(connection) while True: current = ae33.current_measurement() diff --git a/umnp/communication/__init__.py b/umnp/communication/__init__.py index 6f27cff42a0dd982c6ebd8a22b6c55486d7949e3..8b7695d88ee5b6b06f0a46ae36af2c70aa4fa685 100644 --- a/umnp/communication/__init__.py +++ b/umnp/communication/__init__.py @@ -1,5 +1,5 @@ -SERIAL_DEFAULT_BAUD_RATE = 112512 -SERIAL_DEFAULT_WAIT_TIME_S = 0.01 +SERIAL_DEFAULT_BAUD_RATE = 115200 +SERIAL_DEFAULT_WAIT_TIME_S = 0.5 class ConnectionProblemException(Exception): diff --git a/umnp/communication/abstract_serial_connection.py b/umnp/communication/abstract_serial_connection.py index fbd55d138c348e8d98ea3dc70e08132b8d9cec28..710c002a8dc6d7550494729b170a832ac517a450 100644 --- a/umnp/communication/abstract_serial_connection.py +++ b/umnp/communication/abstract_serial_connection.py @@ -6,7 +6,7 @@ from umnp.communication import SERIAL_DEFAULT_BAUD_RATE, SERIAL_DEFAULT_WAIT_TIM class AbstractSerialConnection: def __init__(self, address, options: dict | None = None): self.__connection = None - self.__lock = asyncio.Lock() + self.__address = address if options is None: self.__options = {} @@ -19,6 +19,33 @@ class AbstractSerialConnection: self.__line_sep_write = self.get_option("write separator", b"\r") self.__max_connection_attempts = self.get_option("max connection attempts", 3) + + + @property + def connection(self): + return self.__connection + + @property + def max_connection_attempts(self): + return self.__max_connection_attempts + @property + def address(self): + return self.__address + @property + def wait_time(self): + return self.__wait_time + @property + def line_sep_read(self): + return self.__line_sep_read + @property + def line_sep_write(self): + return self.__line_sep_write + + @connection.setter + def connection(self, conn): + self.__connection = conn + + def connect(self): raise NotImplementedError @@ -29,7 +56,7 @@ class AbstractSerialConnection: raise NotImplementedError def send_command(self, command): - pass + raise NotImplementedError def read_line(self, timeout=None): raise NotImplementedError @@ -37,4 +64,4 @@ class AbstractSerialConnection: def get_option(self, name: str, default=None): if self.__options is None: return default - return self.__options.get(name) + return self.__options.get(name, default) diff --git a/umnp/communication/serial_connection.py b/umnp/communication/serial_connection.py index 3c1834d665d32775d18654e9082f09c62ebd4ed7..e9973907a7c04358bb0a714699a16a87b24053f9 100644 --- a/umnp/communication/serial_connection.py +++ b/umnp/communication/serial_connection.py @@ -1,30 +1,38 @@ +import asyncio import logging +import threading import time import serial -from umnp.communication import ConnectionProblemException +from umnp.communication import ConnectionProblemException, SERIAL_DEFAULT_BAUD_RATE, SERIAL_DEFAULT_WAIT_TIME_S from umnp.communication.abstract_serial_connection import AbstractSerialConnection class SerialConnection(AbstractSerialConnection): def __init__(self, address, options=None): super().__init__(address, options) + self.connect() + self.__lock = threading.RLock() + + @property + def baud_rate(self): + return self.get_option("baudrate", SERIAL_DEFAULT_BAUD_RATE) def connect(self): - if self.__connection: + if self.connection: return attempts = 0 connection = None - max_attempts = self.__max_connection_attempts + max_attempts = self.max_connection_attempts while attempts < max_attempts: try: connection = serial.Serial( - port=self.__address, - baudrate=self.__baud_rate, + port=self.address, + baudrate=self.baud_rate, parity=self.get_option("parity", serial.PARITY_NONE), stopbits=self.get_option("stopbits", serial.STOPBITS_ONE), - timeout=self.__wait_time, + timeout=self.get_option("wait time", SERIAL_DEFAULT_WAIT_TIME_S), xonxoff=self.get_option("xonoff", False), rtscts=self.get_option("rtscts", False), dsrdtr=self.get_option("dsrdtr", False), @@ -39,14 +47,120 @@ class SerialConnection(AbstractSerialConnection): time.sleep(0.1) if not connection: - logging.error(f"could not connect to {self.__address}") + print(f"could not connect to {self.address}") return time.sleep(0.1) - self.__connection = connection + self.connection = connection connection.flush() connection.reset_input_buffer() connection.reset_output_buffer() - logging.info(f"connected to {self.__address}, baud rate: {self.__baud_rate}") + print(f"connected to {self.address}, baud rate: {self.baud_rate}") logging.debug(connection) + + def sync_command(self, cmd, expected_lines=1, show_reply=True): + if not self.connection: + logging.error(f"could not send command '{cmd}': not connected") + return None + time.sleep(self.wait_time) + self.connection.flushInput() + self.connection.flushOutput() + + if isinstance(self.line_sep_read, bytes): + empty = self.line_sep_read.decode('utf8') + elif isinstance(self.line_sep_read, str): + empty = self.line_sep_read + else: + empty = "" + + result = '' + result_debug = "<no response>" + cmd_nice = cmd + print( + f"sending sync command '{cmd_nice}' to '{self.address}' and expecting {expected_lines} lines of results") + + with self.__lock: + self.send_command(cmd) + # time.sleep(self.wait_time) + + if expected_lines == 1: + result = self.read_line(timeout=self.wait_time) + else: + received_lines = 0 + while received_lines < expected_lines: + line = self.read_line(timeout=self.wait_time) + if line and line != empty: + result += line + received_lines += 1 + + if result: + if isinstance(result, bytes): + result = result.decode("utf8") + result_debug = result.replace('\n', '\\n').replace('\r', '\\r') + else: + result_debug = result.replace('\n', '\\n').replace('\r', '\\r') + + if show_reply: + print(f"received (processed) '{result_debug}' from {self.address} in response to '{cmd}'") + else: + print(f"received (processed) '{result_debug}' from {self.address} in response to '{cmd}'") + return result + + def read_line(self, timeout=None): + # fixme: what do we do if we have no connection + if not self.connection: + return None + + result = b'' + # fixme: what do we do on an error? + + with self.__lock: + sep = self.line_sep_read + sep = None + if sep: + start_time = time.time() + try: + while True: + newest_char = self.connection.read(1) + print(newest_char) + if newest_char is not None: + result += newest_char + if newest_char == sep: + break + if time.time() - start_time > timeout: + # break + pass + except serial.serialutil.SerialException: + pass + else: + try: + result = self.connection.readline() + print("Result:", result.decode('utf-8').strip()) + except serial.serialutil.SerialException: + pass + + logging.debug(f"received (raw) '{result}' from {self.address}") + + return result.decode('utf8').strip() + + def send_command(self, cmd): + if not self.connection: + logging.error(f"could not send command '{cmd}': not connected") + return None + + print(f"sending command '{cmd}' to '{self.address}'") + + if isinstance(cmd, str): + cmd = cmd.encode("utf-8") + + if self.line_sep_write: + sep = self.line_sep_write + if isinstance(sep, str): + sep = sep.encode('utf-8') + + cmd += sep + + with self.__lock: + self.connection.write(cmd) + diff --git a/umnp/devices/aethalometer/ae33.py b/umnp/devices/aethalometer/ae33.py index d027b44f2ddbbb24b16db1dfe00fa389d0611afe..60f337ec1aeb1e7698fcc35154b17fd57f62ecb3 100644 --- a/umnp/devices/aethalometer/ae33.py +++ b/umnp/devices/aethalometer/ae33.py @@ -13,7 +13,7 @@ class AE33: pass def request_measurement(self) -> str | None: - return self.__connection.sync_command("$A33:D1\r", 1) + return self.__connection.sync_command(b"$AE33:D1\r", 1) def current_measurement(self) -> str | None: current = self.request_measurement()