From d731954477f6e508bd747a513e13d1b62d738589 Mon Sep 17 00:00:00 2001 From: flxdot Date: Wed, 17 Apr 2024 18:58:50 +0200 Subject: [PATCH 01/57] draft --- .../carlos/edge/device/config.py | 10 +-- .../carlos/edge/device/config_test.py | 3 +- .../carlos/edge/device/runtime.py | 2 +- .../carlos/edge/interface/device/__init__.py | 3 + .../carlos/edge/interface/device/config.py | 64 +++++++++++++++++++ services/device/device/cli/config.py | 2 + 6 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 lib/py_edge_interface/carlos/edge/interface/device/__init__.py create mode 100644 lib/py_edge_interface/carlos/edge/interface/device/config.py diff --git a/lib/py_edge_device/carlos/edge/device/config.py b/lib/py_edge_device/carlos/edge/device/config.py index 945be2c9..cc95a041 100644 --- a/lib/py_edge_device/carlos/edge/device/config.py +++ b/lib/py_edge_device/carlos/edge/device/config.py @@ -2,7 +2,6 @@ configuration of the application.""" __all__ = [ - "DeviceConfig", "read_config", "read_config_file", "write_config", @@ -13,18 +12,11 @@ from typing import TypeVar import yaml -from carlos.edge.interface import DeviceId -from pydantic import BaseModel, Field +from pydantic import BaseModel from carlos.edge.device.constants import CONFIG_FILE_NAME -class DeviceConfig(BaseModel): - """Configures the pure device settings.""" - - device_id: DeviceId = Field(..., description="The unique identifier of the device.") - - Config = TypeVar("Config", bound=BaseModel) diff --git a/lib/py_edge_device/carlos/edge/device/config_test.py b/lib/py_edge_device/carlos/edge/device/config_test.py index 69883af8..00a1db9f 100644 --- a/lib/py_edge_device/carlos/edge/device/config_test.py +++ b/lib/py_edge_device/carlos/edge/device/config_test.py @@ -3,7 +3,8 @@ import pytest -from carlos.edge.device.config import DeviceConfig, read_config_file, write_config_file +from carlos.edge.device.config import read_config_file, write_config_file +from carlos.edge.interface.device import DeviceConfig def test_config_file_io(tmp_path: Path): diff --git a/lib/py_edge_device/carlos/edge/device/runtime.py b/lib/py_edge_device/carlos/edge/device/runtime.py index 14949287..a0aad4d3 100644 --- a/lib/py_edge_device/carlos/edge/device/runtime.py +++ b/lib/py_edge_device/carlos/edge/device/runtime.py @@ -11,7 +11,7 @@ from loguru import logger from .communication import DeviceCommunicationHandler -from .config import DeviceConfig +from carlos.edge.interface.device import DeviceConfig # We don't cover this in the unit tests. This needs to be tested in an integration test. diff --git a/lib/py_edge_interface/carlos/edge/interface/device/__init__.py b/lib/py_edge_interface/carlos/edge/interface/device/__init__.py new file mode 100644 index 00000000..79092ecc --- /dev/null +++ b/lib/py_edge_interface/carlos/edge/interface/device/__init__.py @@ -0,0 +1,3 @@ +__all__ = ["DeviceConfig"] + +from .config import DeviceConfig diff --git a/lib/py_edge_interface/carlos/edge/interface/device/config.py b/lib/py_edge_interface/carlos/edge/interface/device/config.py new file mode 100644 index 00000000..71fbf530 --- /dev/null +++ b/lib/py_edge_interface/carlos/edge/interface/device/config.py @@ -0,0 +1,64 @@ +__all__ = ["DeviceConfig"] +from typing import Literal + +from pydantic import BaseModel, Field + +# Pin layout +# 5V, 5V, GND, 14, 15, 18, GND, 23, 24, GND, 25, 8, 7, ID EEPROM, GND, 12, GND, 16, 20, 21 # Outer pins # noqa +# 3.3V, 2, 3, 4, GND, 17, 27, 22, 3.3V, 10, 9, 11, GND, ID EEPROM, 5, 6, 13, 19, 26, GND # Inner pins # noqa + +PINS = list(range(2, 28)) +"""The Pin numbers available for GPIO communication.""" + + +class GPIOInputConfig(BaseModel): + """Defines a single input configuration.""" + + display_name: str = Field(..., description="User defined name to be displayed in the UI.") + + + + pin: Literal[ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + ] = Field(..., description="The GPIO pin number.") + + +I2C_PINS = [2, 3] +"""The Pin numbers designated for I2C communication.""" + + +class I2CInputConfig(BaseModel): + """Defines a single input configuration.""" + + +class DeviceConfig(BaseModel): + """Configures the pure device settings.""" + + inputs: list[InputConfig] = Field( + default_factory=list, description="The input configuration." + ) diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index 70946016..ee0e983c 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -3,6 +3,8 @@ from typing import TypeVar import typer +from carlos.edge.interface.device import DeviceConfig +from carlos.edge.device.config import read_config, write_config from pydantic import BaseModel from pydantic_core import PydanticUndefinedType from rich import print, print_json From ab56e448d243a80726f2ce0255e71e07bd966b67 Mon Sep 17 00:00:00 2001 From: flxdot Date: Wed, 17 Apr 2024 19:38:09 +0200 Subject: [PATCH 02/57] create config --- .../carlos/edge/interface/device/config.py | 126 ++++++++++++++++-- 1 file changed, 117 insertions(+), 9 deletions(-) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/config.py b/lib/py_edge_interface/carlos/edge/interface/device/config.py index 71fbf530..9f2f4735 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/config.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/config.py @@ -1,22 +1,43 @@ __all__ = ["DeviceConfig"] + from typing import Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_validator, model_validator # Pin layout # 5V, 5V, GND, 14, 15, 18, GND, 23, 24, GND, 25, 8, 7, ID EEPROM, GND, 12, GND, 16, 20, 21 # Outer pins # noqa # 3.3V, 2, 3, 4, GND, 17, 27, 22, 3.3V, 10, 9, 11, GND, ID EEPROM, 5, 6, 13, 19, 26, GND # Inner pins # noqa -PINS = list(range(2, 28)) -"""The Pin numbers available for GPIO communication.""" +ProtocolConfig = Literal["i2c", "gpio"] -class GPIOInputConfig(BaseModel): - """Defines a single input configuration.""" +class IOConfig(BaseModel): + """Common base class for all IO configurations.""" + + identifier: str = Field( + ..., description="A unique identifier for the IO configuration. " + "It is used to allow chaning addresses, pins if required later." + ) + + protocol: ProtocolConfig = Field( + ..., + description="The communication protocol to be used for the IO.", + ) + + type: str = Field( + ..., + description="A string that uniquely identifies the type of IO. Usually the " + "name of the sensor or actuator in lower case letters.", + ) - display_name: str = Field(..., description="User defined name to be displayed in the UI.") +class GPIOConfig(IOConfig): + """Defines a single input configuration.""" + protocol: Literal["gpio"] = Field( + "gpio", + description="The communication protocol to be used for the IO.", + ) pin: Literal[ 2, @@ -48,17 +69,104 @@ class GPIOInputConfig(BaseModel): ] = Field(..., description="The GPIO pin number.") +class DigitalGPIOOutputConfig(GPIOConfig): + """Defines a single digital output configuration.""" + + direction: Literal["output"] = Field( + ..., description="The direction of the GPIO pin." + ) + + I2C_PINS = [2, 3] """The Pin numbers designated for I2C communication.""" -class I2CInputConfig(BaseModel): +class I2CConfig(BaseModel): """Defines a single input configuration.""" + protocol: Literal["i2c"] = Field( + "i2c", + description="The communication protocol to be used for the IO.", + ) + + address: str = Field(..., description="The I2C address of the device.") + + @field_validator("address", mode="before") + def validate_address(cls, value): + """Validate the I2C address.""" + + if isinstance(value, str): + if value.startswith("0x"): + value = value[2:] + + try: + value = int(value, 16) + except ValueError: + raise ValueError("The I2C address must be a valid hexadecimal value.") + + # first 2 are reserved + # address length is 7 bits, but the majority of literature defines 0x77 as max + if not 0x03 <= int(value) <= 0x77: + raise ValueError("The valid I2C address range is 0x03 to 0x77.") + + return hex(value) + class DeviceConfig(BaseModel): """Configures the pure device settings.""" - inputs: list[InputConfig] = Field( - default_factory=list, description="The input configuration." + io: list[GPIOConfig | I2CConfig] = Field( + default_factory=list, description="A list of IO configurations." ) + + @model_validator(mode="after") + def _validate_address_or_pin_overlap(self): + """This function ensures that the configured pins and addresses are unique.""" + gpio_configs = [io for io in self.io if isinstance(io, GPIOConfig)] + + # Ensure GPIO pins are unique + seen_pins = set() + duplicate_gpio_pins = [ + gpio.pin + for gpio in gpio_configs + if gpio.pin in seen_pins or seen_pins.add(gpio.pin) + ] + if duplicate_gpio_pins: + raise ValueError( + f"The GPIO pins {duplicate_gpio_pins} are configured more than once." + f"Please ensure that each GPIO pin is configured only once." + ) + + i2c_configs = [io for io in self.io if isinstance(io, I2CConfig)] + if i2c_configs: + if any(gpio.pin in I2C_PINS for gpio in gpio_configs): + raise ValueError( + "The GPIO pins 2 and 3 are reserved for I2C communication." + "Please use other pins for GPIO configuration." + ) + + # Ensure I2C addresses are unique + seen_addresses = set() + duplicate_i2c_addresses = [ + i2c.address + for i2c in i2c_configs + if i2c.address in i2c_configs or seen_addresses.add(i2c.address) + ] + if duplicate_i2c_addresses: + raise ValueError( + f"The I2C addresses {duplicate_i2c_addresses} are configured more than " + f"once. Please ensure that each I2C address is configured only once." + ) + + # Ensure all identifiers are unique + seen_identifiers = set() + duplicate_identifiers = [ + io.identifier + for io in self.io + if io.identifier in seen_identifiers or seen_identifiers.add(io.identifier) + ] + if duplicate_identifiers: + raise ValueError( + f"The identifiers {duplicate_identifiers} are configured more than " + f"once. Please ensure that each identifier is configured only once." + ) From b869feaa866a8c6ebedf4455b576a6d94e3f7f38 Mon Sep 17 00:00:00 2001 From: flxdot Date: Wed, 17 Apr 2024 20:19:37 +0200 Subject: [PATCH 03/57] fix config --- .../carlos/edge/interface/device/config.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/config.py b/lib/py_edge_interface/carlos/edge/interface/device/config.py index 9f2f4735..6cccf784 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/config.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/config.py @@ -15,8 +15,9 @@ class IOConfig(BaseModel): """Common base class for all IO configurations.""" identifier: str = Field( - ..., description="A unique identifier for the IO configuration. " - "It is used to allow chaning addresses, pins if required later." + ..., + description="A unique identifier for the IO configuration. " + "It is used to allow changing addresses, pins if required later.", ) protocol: ProtocolConfig = Field( @@ -27,7 +28,11 @@ class IOConfig(BaseModel): type: str = Field( ..., description="A string that uniquely identifies the type of IO. Usually the " - "name of the sensor or actuator in lower case letters.", + "name of the sensor or actuator in lower case letters.", + ) + + direction: Literal["input", "output"] = Field( + ..., description="The direction of the IO." ) From 27fc5f419a6883454f9565a54f30a2a05f8c0fa0 Mon Sep 17 00:00:00 2001 From: flxdot Date: Wed, 17 Apr 2024 22:31:52 +0200 Subject: [PATCH 04/57] add interface to define IO config --- .../carlos/edge/device/config.py | 1 - .../carlos/edge/device/config_test.py | 2 +- .../carlos/edge/device/io/__init__.py | 0 .../carlos/edge/device/io/device_metrics.py | 32 +++++++++++++ .../carlos/edge/device/io/dht11.py | 0 .../carlos/edge/device/io/relay.py | 0 .../carlos/edge/device/io/si1145.py | 0 .../carlos/edge/device/runtime.py | 2 +- lib/py_edge_device/poetry.lock | 30 +++++++++++- lib/py_edge_device/pyproject.toml | 1 + .../carlos/edge/interface/device/__init__.py | 16 ++++++- .../carlos/edge/interface/device/config.py | 19 ++++---- .../carlos/edge/interface/device/io.py | 48 +++++++++++++++++++ .../edge/interface/device/peripheral.py | 43 +++++++++++++++++ 14 files changed, 177 insertions(+), 17 deletions(-) create mode 100644 lib/py_edge_device/carlos/edge/device/io/__init__.py create mode 100644 lib/py_edge_device/carlos/edge/device/io/device_metrics.py create mode 100644 lib/py_edge_device/carlos/edge/device/io/dht11.py create mode 100644 lib/py_edge_device/carlos/edge/device/io/relay.py create mode 100644 lib/py_edge_device/carlos/edge/device/io/si1145.py create mode 100644 lib/py_edge_interface/carlos/edge/interface/device/io.py create mode 100644 lib/py_edge_interface/carlos/edge/interface/device/peripheral.py diff --git a/lib/py_edge_device/carlos/edge/device/config.py b/lib/py_edge_device/carlos/edge/device/config.py index cc95a041..67c3abdc 100644 --- a/lib/py_edge_device/carlos/edge/device/config.py +++ b/lib/py_edge_device/carlos/edge/device/config.py @@ -16,7 +16,6 @@ from carlos.edge.device.constants import CONFIG_FILE_NAME - Config = TypeVar("Config", bound=BaseModel) diff --git a/lib/py_edge_device/carlos/edge/device/config_test.py b/lib/py_edge_device/carlos/edge/device/config_test.py index 00a1db9f..f0c3d160 100644 --- a/lib/py_edge_device/carlos/edge/device/config_test.py +++ b/lib/py_edge_device/carlos/edge/device/config_test.py @@ -2,9 +2,9 @@ from uuid import uuid4 import pytest +from carlos.edge.interface.device import DeviceConfig from carlos.edge.device.config import read_config_file, write_config_file -from carlos.edge.interface.device import DeviceConfig def test_config_file_io(tmp_path: Path): diff --git a/lib/py_edge_device/carlos/edge/device/io/__init__.py b/lib/py_edge_device/carlos/edge/device/io/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lib/py_edge_device/carlos/edge/device/io/device_metrics.py b/lib/py_edge_device/carlos/edge/device/io/device_metrics.py new file mode 100644 index 00000000..e70c1a30 --- /dev/null +++ b/lib/py_edge_device/carlos/edge/device/io/device_metrics.py @@ -0,0 +1,32 @@ +import psutil +from carlos.edge.interface.device import AnalogInput, IOConfig, peripheral_registry + + +class DeviceMetrics(AnalogInput): + """Provides the metrics of the device.""" + + def __init__(self, config: IOConfig): + + super().__init__(config=config) + + def read(self) -> dict[str, float]: + """Reads the device metrics.""" + + return { + "cpu.load_percent": psutil.cpu_percent(interval=1.0), + "cpu.temperature": self._read_cpu_temp(), + "memory.usage_percent": psutil.virtual_memory().percent, + "disk.usage_percent": psutil.disk_usage("/").percent, + } + + @staticmethod + def _read_cpu_temp() -> float: + """Reads the CPU temperature.""" + try: + with open("/sys/class/thermal/thermal_zone0/temp") as f: + return float(f.read().strip()) / 1000 + except FileNotFoundError: + return 0.0 + + +peripheral_registry.register(ptype=__name__, config=IOConfig, factory=DeviceMetrics) diff --git a/lib/py_edge_device/carlos/edge/device/io/dht11.py b/lib/py_edge_device/carlos/edge/device/io/dht11.py new file mode 100644 index 00000000..e69de29b diff --git a/lib/py_edge_device/carlos/edge/device/io/relay.py b/lib/py_edge_device/carlos/edge/device/io/relay.py new file mode 100644 index 00000000..e69de29b diff --git a/lib/py_edge_device/carlos/edge/device/io/si1145.py b/lib/py_edge_device/carlos/edge/device/io/si1145.py new file mode 100644 index 00000000..e69de29b diff --git a/lib/py_edge_device/carlos/edge/device/runtime.py b/lib/py_edge_device/carlos/edge/device/runtime.py index a0aad4d3..9fc319a1 100644 --- a/lib/py_edge_device/carlos/edge/device/runtime.py +++ b/lib/py_edge_device/carlos/edge/device/runtime.py @@ -7,11 +7,11 @@ from apscheduler import AsyncScheduler from apscheduler.triggers.interval import IntervalTrigger from carlos.edge.interface import EdgeConnectionDisconnected, EdgeProtocol +from carlos.edge.interface.device import DeviceConfig from carlos.edge.interface.protocol import PING from loguru import logger from .communication import DeviceCommunicationHandler -from carlos.edge.interface.device import DeviceConfig # We don't cover this in the unit tests. This needs to be tested in an integration test. diff --git a/lib/py_edge_device/poetry.lock b/lib/py_edge_device/poetry.lock index 96ec1d33..062afd6c 100644 --- a/lib/py_edge_device/poetry.lock +++ b/lib/py_edge_device/poetry.lock @@ -637,6 +637,34 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "psutil" +version = "5.9.8" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, + {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, + {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, + {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, + {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, + {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, + {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, + {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, + {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, + {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + [[package]] name = "pydantic" version = "2.7.0" @@ -1190,4 +1218,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.12" -content-hash = "1567e5e5cfd6b309a9ad9da8f8c1d9130717d3a8683d4c39b7f380ec62a82f83" +content-hash = "755a94aa0c1917818339268a3a4185ec903aac201a008a812892ae8f7e3ffe22" diff --git a/lib/py_edge_device/pyproject.toml b/lib/py_edge_device/pyproject.toml index de3e1ec8..9a6c61dd 100644 --- a/lib/py_edge_device/pyproject.toml +++ b/lib/py_edge_device/pyproject.toml @@ -16,6 +16,7 @@ apscheduler = "^4.0.0a4" pydantic = "^2.6.4" pyyaml = "^6.0.1" "carlos.edge.interface" = {path = "../py_edge_interface"} +psutil = "^5.9.8" [tool.poetry.group.dev.dependencies] "devtools" = {path = "../py_dev_dependencies"} diff --git a/lib/py_edge_interface/carlos/edge/interface/device/__init__.py b/lib/py_edge_interface/carlos/edge/interface/device/__init__.py index 79092ecc..116f70cf 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/__init__.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/__init__.py @@ -1,3 +1,15 @@ -__all__ = ["DeviceConfig"] +__all__ = [ + "AnalogInput", + "CarlosIO", + "DeviceConfig", + "DigitalOutput", + "GPIOConfig", + "I2CConfig", + "IOConfig", + "PeripheralConfig", + "peripheral_registry", +] -from .config import DeviceConfig +from .config import DeviceConfig, GPIOConfig, I2CConfig, IOConfig, PeripheralConfig +from .io import peripheral_registry +from .peripheral import AnalogInput, CarlosIO, DigitalOutput diff --git a/lib/py_edge_interface/carlos/edge/interface/device/config.py b/lib/py_edge_interface/carlos/edge/interface/device/config.py index 6cccf784..350dbe57 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/config.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/config.py @@ -1,5 +1,6 @@ -__all__ = ["DeviceConfig"] +__all__ = ["DeviceConfig", "GPIOConfig", "I2CConfig", "PeripheralConfig"] +from abc import ABC from typing import Literal from pydantic import BaseModel, Field, field_validator, model_validator @@ -8,10 +9,8 @@ # 5V, 5V, GND, 14, 15, 18, GND, 23, 24, GND, 25, 8, 7, ID EEPROM, GND, 12, GND, 16, 20, 21 # Outer pins # noqa # 3.3V, 2, 3, 4, GND, 17, 27, 22, 3.3V, 10, 9, 11, GND, ID EEPROM, 5, 6, 13, 19, 26, GND # Inner pins # noqa -ProtocolConfig = Literal["i2c", "gpio"] - -class IOConfig(BaseModel): +class IOConfig(BaseModel, ABC): """Common base class for all IO configurations.""" identifier: str = Field( @@ -20,12 +19,7 @@ class IOConfig(BaseModel): "It is used to allow changing addresses, pins if required later.", ) - protocol: ProtocolConfig = Field( - ..., - description="The communication protocol to be used for the IO.", - ) - - type: str = Field( + ptype: str = Field( ..., description="A string that uniquely identifies the type of IO. Usually the " "name of the sensor or actuator in lower case letters.", @@ -117,10 +111,13 @@ def validate_address(cls, value): return hex(value) +PeripheralConfig = GPIOConfig | I2CConfig | IOConfig + + class DeviceConfig(BaseModel): """Configures the pure device settings.""" - io: list[GPIOConfig | I2CConfig] = Field( + io: list[PeripheralConfig] = Field( default_factory=list, description="A list of IO configurations." ) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/io.py b/lib/py_edge_interface/carlos/edge/interface/device/io.py new file mode 100644 index 00000000..8970f14d --- /dev/null +++ b/lib/py_edge_interface/carlos/edge/interface/device/io.py @@ -0,0 +1,48 @@ +__all__ = ["peripheral_registry"] + +from collections import namedtuple +from typing import Callable, TypedDict, TypeVar + +from .config import PeripheralConfig +from .peripheral import CarlosIO + +C = TypeVar("C", bound=PeripheralConfig) + +RegistryItem = namedtuple("RegistryItem", ["config", "factory"]) + + +class ConfigDict(TypedDict): + ptype: str + + +class PeripheralRegistry: + def __init__(self): + self._peripherals: dict[str, RegistryItem] = {} + + def register(self, ptype: str, config: type[C], factory: Callable[[C], CarlosIO]): + """Registers a peripheral with the peripheral registry. + + :param ptype: The peripheral type. + :param config: The peripheral configuration model. + :param factory: The peripheral factory function. + """ + + if ptype in self._peripherals: + raise ValueError(f"The peripheral {ptype} is already registered.") + + self._peripherals[ptype] = RegistryItem(config, factory) + + def build(self, config: ConfigDict) -> CarlosIO: + """Builds a peripheral from the peripheral registry.""" + + ptype = config["ptype"] + + if type not in self._peripherals: + raise ValueError(f"The peripheral {ptype} is not registered.") + + entry = self._peripherals[ptype] + + return entry.factory(entry.config.model_validate(config)) + + +peripheral_registry = PeripheralRegistry() diff --git a/lib/py_edge_interface/carlos/edge/interface/device/peripheral.py b/lib/py_edge_interface/carlos/edge/interface/device/peripheral.py new file mode 100644 index 00000000..ec0cc243 --- /dev/null +++ b/lib/py_edge_interface/carlos/edge/interface/device/peripheral.py @@ -0,0 +1,43 @@ +__all__ = ["AnalogInput", "DigitalOutput", "CarlosIO"] +import asyncio +import concurrent.futures +from abc import ABC, abstractmethod + +from carlos.edge.interface.device.config import PeripheralConfig + + +class CarlosPeripheral(ABC): + """Common base class for all peripherals.""" + + def __init__(self, config: PeripheralConfig): + self.config = config + + +class AnalogInput(CarlosPeripheral, ABC): + """Common base class for all analog input peripherals.""" + + @abstractmethod + def read(self) -> dict[str, float]: + """Reads the value of the analog input. The return value is a dictionary + containing the value of the analog input.""" + pass + + async def read_async(self) -> dict[str, float]: + """Reads the value of the analog input asynchronously. The return value is a dictionary + containing the value of the analog input.""" + + loop = asyncio.get_running_loop() + + with concurrent.futures.ThreadPoolExecutor() as pool: + return await loop.run_in_executor(executor=pool, func=self.read) + + +class DigitalOutput(CarlosPeripheral, ABC): + """Common base class for all digital output peripherals.""" + + @abstractmethod + def set(self, value: bool): + pass + + +CarlosIO = AnalogInput | DigitalOutput From 702d1881418fd45ff23f3fca52a76e958e5553cb Mon Sep 17 00:00:00 2001 From: flxdot Date: Wed, 17 Apr 2024 23:46:53 +0200 Subject: [PATCH 05/57] add some more actual code --- .../carlos/edge/device/io/_dhtxx.py | 153 +++++++++++++++++ .../carlos/edge/device/io/device_metrics.py | 4 +- .../carlos/edge/device/protocol/__init__.py | 0 .../carlos/edge/device/protocol/gpio.py | 6 + .../carlos/edge/device/protocol/i2c.py | 158 ++++++++++++++++++ lib/py_edge_device/poetry.lock | 43 ++++- lib/py_edge_device/pyproject.toml | 3 + 7 files changed, 364 insertions(+), 3 deletions(-) create mode 100644 lib/py_edge_device/carlos/edge/device/io/_dhtxx.py create mode 100644 lib/py_edge_device/carlos/edge/device/protocol/__init__.py create mode 100644 lib/py_edge_device/carlos/edge/device/protocol/gpio.py create mode 100644 lib/py_edge_device/carlos/edge/device/protocol/i2c.py diff --git a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py new file mode 100644 index 00000000..81e81cb6 --- /dev/null +++ b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py @@ -0,0 +1,153 @@ +from enum import StrEnum + + +class DHTType(StrEnum): + DHT11 = "DHT11" + DHT22 = "DHT22" + + +class DHT: + """Code for Temperature & Humidity Sensor of Seeed Studio. + + Code is originally from, but modified to my needs: + http://wiki.seeedstudio.com/Grove-TemperatureAndHumidity_Sensor/ + """ + + PULSES_CNT = 41 + + MAX_CNT = 320 + + def __init__(self, dht_type: DHTType, pin: int): + """ + + :param dht_type: either DHTtype.DHT11 or DHTtype.22 + :param pin: gpio pin where the sensor is connected to + """ + + # store the pin and type + self.pin = pin + self.dht_type = dht_type + + # setup the GPIO mode + GPIO.setmode(GPIO.BCM) + GPIO.setwarnings(False) + GPIO.setup(self.pin, GPIO.OUT) + + @property + def dht_type(self): + return self._dht_type + + def _read(self): + """Internal read method. + + :returns (humidity in %, temperature in °C)""" + + # Send Falling signal to trigger sensor output data + # Wait for 20ms to collect 42 bytes data + GPIO.setup(self.pin, GPIO.OUT) + set_max_priority() + + GPIO.output(self.pin, GPIO.HIGH) + sleep(0.2) + + GPIO.output(self.pin, GPIO.LOW) + sleep(0.018) + + GPIO.setup(self.pin, GPIO.IN) + + # a short delay needed + for i in range(10): + pass + + # pullup by host 20-40 us + count = 0 + while GPIO.input(self.pin): + count += 1 + if count > self.MAX_CNT: + # print("pullup by host 20-40us failed") + set_default_priority() + return None, "pullup by host 20-40us failed" + + pulse_cnt = [0] * (2 * self.PULSES_CNT) + fix_crc = False + for i in range(0, self.PULSES_CNT * 2, 2): + while not GPIO.input(self.pin): + pulse_cnt[i] += 1 + if pulse_cnt[i] > self.MAX_CNT: + # print("pulldown by DHT timeout %d" % i)) + set_default_priority() + return None, "pulldown by DHT timeout {}".format(i) + + while GPIO.input(self.pin): + pulse_cnt[i + 1] += 1 + if pulse_cnt[i + 1] > self.MAX_CNT: + # print("pullup by DHT timeout {}".format((i + 1))) + if i == (self.PULSES_CNT - 1) * 2: + # fix_crc = True + # break + pass + set_default_priority() + return None, "pullup by DHT timeout {}".format(i) + + # back to normal priority + set_default_priority() + + total_cnt = 0 + for i in range(2, 2 * self.PULSES_CNT, 2): + total_cnt += pulse_cnt[i] + + # Low level ( 50 us) average counter + average_cnt = total_cnt / (self.PULSES_CNT - 1) + # print("low level average loop = {}".format(average_cnt)) + + data = "" + for i in range(3, 2 * self.PULSES_CNT, 2): + if pulse_cnt[i] > average_cnt: + data += "1" + else: + data += "0" + + data0 = int(data[0:8], 2) + data1 = int(data[8:16], 2) + data2 = int(data[16:24], 2) + data3 = int(data[24:32], 2) + data4 = int(data[32:40], 2) + + if fix_crc and data4 != ((data0 + data1 + data2 + data3) & 0xFF): + data4 = data4 ^ 0x01 + data = data[0 : self.PULSES_CNT - 2] + ("1" if data4 & 0x01 else "0") + + if data4 == ((data0 + data1 + data2 + data3) & 0xFF): + if self._dht_type == DHTtype.DHT11: + humi = int(data0) + temp = int(data2) + elif self._dht_type == DHTtype.DHT22: + humi = float(int(data[0:16], 2) * 0.1) + temp = float(int(data[17:32], 2) * 0.2 * (0.5 - int(data[16], 2))) + else: + # print("checksum error!") + return None, "checksum error!" + + return humi, temp + + def read(self, retries=15): + for i in range(retries): + humi, temp = self._read() + if humi is not None: + break + if humi is None: + return self._last_humi, self._last_temp + self._last_humi, self._last_temp = humi, temp + return humi, temp + + def measure(self): + """Performs a measurement and returns all available values in a dictionary. + The keys() are the names of the measurement and the values the corresponding values. + + :return: dict + """ + + humi, temp = self.read() + if humi == 0 and temp == 0: + return {"humidity": None, "temperature": None} + return {"humidity": float(humi), "temperature": float(temp)} diff --git a/lib/py_edge_device/carlos/edge/device/io/device_metrics.py b/lib/py_edge_device/carlos/edge/device/io/device_metrics.py index e70c1a30..567a59f8 100644 --- a/lib/py_edge_device/carlos/edge/device/io/device_metrics.py +++ b/lib/py_edge_device/carlos/edge/device/io/device_metrics.py @@ -1,5 +1,6 @@ import psutil from carlos.edge.interface.device import AnalogInput, IOConfig, peripheral_registry +from gpiozero import CPUTemperature class DeviceMetrics(AnalogInput): @@ -23,8 +24,7 @@ def read(self) -> dict[str, float]: def _read_cpu_temp() -> float: """Reads the CPU temperature.""" try: - with open("/sys/class/thermal/thermal_zone0/temp") as f: - return float(f.read().strip()) / 1000 + return CPUTemperature().temperature except FileNotFoundError: return 0.0 diff --git a/lib/py_edge_device/carlos/edge/device/protocol/__init__.py b/lib/py_edge_device/carlos/edge/device/protocol/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lib/py_edge_device/carlos/edge/device/protocol/gpio.py b/lib/py_edge_device/carlos/edge/device/protocol/gpio.py new file mode 100644 index 00000000..96b7e131 --- /dev/null +++ b/lib/py_edge_device/carlos/edge/device/protocol/gpio.py @@ -0,0 +1,6 @@ +__all__ = ["GPIO"] + +try: + from RPi import GPIO +except ImportError: + from RPiSim import GPIO diff --git a/lib/py_edge_device/carlos/edge/device/protocol/i2c.py b/lib/py_edge_device/carlos/edge/device/protocol/i2c.py new file mode 100644 index 00000000..09d1a6de --- /dev/null +++ b/lib/py_edge_device/carlos/edge/device/protocol/i2c.py @@ -0,0 +1,158 @@ +#!/usr/bin/python + +import re +from threading import RLock +from typing import Sequence + +import smbus2 + + +class i2cLock: + """Use this i2c lock to prevent simultaneous access of the i2c bus by different + threads.""" + + instance = None + + def __new__(cls): + if not i2cLock.instance: + i2cLock.instance = RLock() + return i2cLock.instance + + +class I2C: + """This class is based on, but heavily modified from, the Adafruit_I2C class""" + + def __init__(self, address: int, bus: int | None = None): + """Creates an instance of the I2C class. + + :param address: The I2C address of the device. + :param bus: The I2C bus number. If None, the bus number is auto-detected. + """ + + self.address = address + # By default, the correct I2C bus is auto-detected using /proc/cpuinfo + # Alternatively, you can hard-code the bus version below: + # self.bus = smbus2.SMBus(0); # Force I2C0 (early 256MB Pi's) + # self.bus = smbus2.SMBus(1); # Force I2C1 (512MB Pi's) + self.bus = smbus2.SMBus( + bus=bus if bus is not None else I2C.get_pi_i2v_bus_number() + ) + + def write8(self, register: int, value: int): + """Writes an 8-bit value to the specified register/address""" + try: + self.bus.write_byte_data( + i2c_addr=self.address, register=register, value=value + ) + except IOError: + raise IOError( + f"Error accessing 0x{self.address:0x}: Check your I2C address." + ) + + def write16(self, register: int, value: int): + """Writes a 16-bit value to the specified register/address pair""" + try: + self.bus.write_word_data( + i2c_addr=self.address, register=register, value=value + ) + except IOError: + raise IOError( + f"Error accessing 0x{self.address:0x}: Check your I2C address." + ) + + def write_raw8(self, value: int): + """Writes an 8-bit value on the bus""" + try: + self.bus.write_byte(i2c_addr=self.address, value=value) + except IOError: + raise IOError( + f"Error accessing 0x{self.address:0x}: Check your I2C address." + ) + + def write_list(self, register: int, data: Sequence[int]): + """Writes an array of bytes using I2C format""" + try: + self.bus.write_i2c_block_data( + i2c_addr=self.address, register=register, data=data + ) + except IOError: + raise IOError( + f"Error accessing 0x{self.address:0x}: Check your I2C address." + ) + + def read_list(self, register: int, length: int): + """Read a list of bytes from the I2C device""" + try: + return self.bus.read_i2c_block_data( + i2c_addr=self.address, register=register, length=length + ) + except IOError: + raise IOError( + f"Error accessing 0x{self.address:0x}: Check your I2C address." + ) + + def read_uint8(self, register: int): + """Read an unsigned byte from the I2C device""" + try: + return self.bus.read_byte_data(i2c_addr=self.address, register=register) + except IOError: + raise IOError( + f"Error accessing 0x{self.address:0x}: Check your I2C address." + ) + + def read_int8(self, register: int): + """Reads a signed byte from the I2C device""" + result = self.read_uint8(register=register) + if result > 127: + result -= 256 + return result + + def read_uint16(self, register: int, little_endian: bool = True): + """Reads an unsigned 16-bit value from the I2C device""" + try: + result = self.bus.read_word_data(i2c_addr=self.address, register=register) + # Swap bytes if using big endian because read_word_data assumes little + # endian on ARM (little endian) systems. + if not little_endian: + result = ((result << 8) & 0xFF00) + (result >> 8) + return result + except IOError: + raise IOError( + f"Error accessing 0x{self.address:0x}: Check your I2C address." + ) + + def read_int16(self, register: int, little_endian=True): + """Reads a signed 16-bit value from the I2C device""" + result = self.read_uint16(register=register, little_endian=little_endian) + if result > 32767: + result -= 65536 + return result + + @staticmethod + def get_pi_revision() -> int: + """Gets the version number of the Raspberry Pi board""" + # Revision list available at: + # http://elinux.org/RPi_HardwareHistory#Board_Revision_History + try: + with open("/proc/cpuinfo", "r") as infile: + for line in infile: + # Match a line of the form "Revision : 0002" while ignoring extra + # info in front of the revsion + # (like 1000 when the Pi was over-volted). + match = re.match("Revision\s+:\s+.*(\w{4})$", line) + if match and match.group(1) in ["0000", "0002", "0003"]: + # Return revision 1 if revision ends with 0000, 0002 or 0003. + return 1 + elif match: + # Assume revision 2 if revision ends with any other 4 chars. + return 2 + # Couldn't find the revision, assume revision 0 like older code for + # compatibility. + return 0 + except: + return 0 + + @staticmethod + def get_pi_i2v_bus_number() -> int: + # Gets the I2C bus number /dev/i2c# + return 1 if I2C.get_pi_revision() > 1 else 0 diff --git a/lib/py_edge_device/poetry.lock b/lib/py_edge_device/poetry.lock index 062afd6c..78eb4e7b 100644 --- a/lib/py_edge_device/poetry.lock +++ b/lib/py_edge_device/poetry.lock @@ -403,6 +403,16 @@ websocket-client = ">=0.32.0" [package.extras] ssh = ["paramiko (>=2.4.3)"] +[[package]] +name = "gpiosimulator" +version = "0.1" +description = "Raspberry Pi GPIO simulator" +optional = false +python-versions = "*" +files = [ + {file = "GPIOSimulator-0.1.tar.gz", hash = "sha256:08a221d03c9c5bd137d573b24aa0ebb9871760b12b8a1090392cbded3d06fee8"}, +] + [[package]] name = "greenlet" version = "3.0.3" @@ -968,6 +978,21 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "rpi-gpio" +version = "0.7.1" +description = "A module to control Raspberry Pi GPIO channels" +optional = false +python-versions = "*" +files = [ + {file = "RPi.GPIO-0.7.1-cp27-cp27mu-linux_armv6l.whl", hash = "sha256:b86b66dc02faa5461b443a1e1f0c1d209d64ab5229696f32fb3b0215e0600c8c"}, + {file = "RPi.GPIO-0.7.1-cp310-cp310-linux_armv6l.whl", hash = "sha256:57b6c044ef5375a78c8dda27cdfadf329e76aa6943cd6cffbbbd345a9adf9ca5"}, + {file = "RPi.GPIO-0.7.1-cp37-cp37m-linux_armv6l.whl", hash = "sha256:77afb817b81331ce3049a4b8f94a85e41b7c404d8e56b61ac0f1eb75c3120868"}, + {file = "RPi.GPIO-0.7.1-cp38-cp38-linux_armv6l.whl", hash = "sha256:29226823da8b5ccb9001d795a944f2e00924eeae583490f0bc7317581172c624"}, + {file = "RPi.GPIO-0.7.1-cp39-cp39-linux_armv6l.whl", hash = "sha256:15311d3b063b71dee738cd26570effc9985a952454d162937c34e08c0fc99902"}, + {file = "RPi.GPIO-0.7.1.tar.gz", hash = "sha256:cd61c4b03c37b62bba4a5acfea9862749c33c618e0295e7e90aa4713fb373b70"}, +] + [[package]] name = "ruff" version = "0.3.7" @@ -1005,6 +1030,22 @@ files = [ {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, ] +[[package]] +name = "smbus2" +version = "0.4.3" +description = "smbus2 is a drop-in replacement for smbus-cffi/smbus-python in pure Python" +optional = false +python-versions = "*" +files = [ + {file = "smbus2-0.4.3-py2.py3-none-any.whl", hash = "sha256:a2fc29cfda4081ead2ed61ef2c4fc041d71dd40a8d917e85216f44786fca2d1d"}, + {file = "smbus2-0.4.3.tar.gz", hash = "sha256:36f2288a8e1a363cb7a7b2244ec98d880eb5a728a2494ac9c71e9de7bf6a803a"}, +] + +[package.extras] +docs = ["sphinx (>=1.5.3)"] +qa = ["flake8"] +test = ["mock", "nose"] + [[package]] name = "sniffio" version = "1.3.1" @@ -1218,4 +1259,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.12" -content-hash = "755a94aa0c1917818339268a3a4185ec903aac201a008a812892ae8f7e3ffe22" +content-hash = "166a5a92157a481abe4e251803a12482d296a956307b2308c9e9d61e28e651af" diff --git a/lib/py_edge_device/pyproject.toml b/lib/py_edge_device/pyproject.toml index 9a6c61dd..bdf9dae9 100644 --- a/lib/py_edge_device/pyproject.toml +++ b/lib/py_edge_device/pyproject.toml @@ -17,6 +17,9 @@ pydantic = "^2.6.4" pyyaml = "^6.0.1" "carlos.edge.interface" = {path = "../py_edge_interface"} psutil = "^5.9.8" +gpiosimulator = "^0.1" +rpi-gpio = {version = "^0.7.1", markers = "platform_machine == 'armv7l' or platform_machine == 'aarch64'"} +smbus2 = "^0.4.3" [tool.poetry.group.dev.dependencies] "devtools" = {path = "../py_dev_dependencies"} From 55ae355d7e98d0609ff71558f6fe728d0c8d688f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 Apr 2024 21:54:39 +0000 Subject: [PATCH 06/57] update services/device Co-authored-by: flxdot --- services/device/device/cli/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index ee0e983c..70946016 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -3,8 +3,6 @@ from typing import TypeVar import typer -from carlos.edge.interface.device import DeviceConfig -from carlos.edge.device.config import read_config, write_config from pydantic import BaseModel from pydantic_core import PydanticUndefinedType from rich import print, print_json From 26a6b74425f52f4c1e2e15457219f958b976f144 Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 00:23:17 +0200 Subject: [PATCH 07/57] add DHT sensor --- .../carlos/edge/device/io/_dhtxx.py | 137 ++++++------------ .../carlos/edge/device/io/device_metrics.py | 4 +- .../carlos/edge/device/io/dht11.py | 35 +++++ .../carlos/edge/device/protocol/__init__.py | 4 + .../carlos/edge/device/protocol/gpio.py | 2 +- .../carlos/edge/device/protocol/i2c.py | 19 +-- 6 files changed, 96 insertions(+), 105 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py index 81e81cb6..dc434bfd 100644 --- a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py +++ b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py @@ -1,4 +1,7 @@ from enum import StrEnum +from time import sleep + +from carlos.edge.device.protocol import GPIO class DHTType(StrEnum): @@ -25,129 +28,85 @@ def __init__(self, dht_type: DHTType, pin: int): """ # store the pin and type - self.pin = pin - self.dht_type = dht_type + self._pin = pin + self._dht_type = dht_type # setup the GPIO mode GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) - GPIO.setup(self.pin, GPIO.OUT) - - @property - def dht_type(self): - return self._dht_type + GPIO.setup(self._pin, GPIO.OUT) - def _read(self): + def read(self) -> tuple[int | float, int | float]: """Internal read method. + http://www.ocfreaks.com/basics-interfacing-dht11-dht22-humidity-temperature-sensor-mcu/ + :returns (humidity in %, temperature in °C)""" # Send Falling signal to trigger sensor output data # Wait for 20ms to collect 42 bytes data - GPIO.setup(self.pin, GPIO.OUT) - set_max_priority() - - GPIO.output(self.pin, GPIO.HIGH) + GPIO.setup(self._pin, GPIO.OUT) + GPIO.output(self._pin, GPIO.HIGH) sleep(0.2) - - GPIO.output(self.pin, GPIO.LOW) + GPIO.output(self._pin, GPIO.LOW) sleep(0.018) - GPIO.setup(self.pin, GPIO.IN) + GPIO.setup(self._pin, GPIO.IN) # a short delay needed - for i in range(10): + for _ in range(10): pass # pullup by host 20-40 us count = 0 - while GPIO.input(self.pin): + while GPIO.input(self._pin): count += 1 if count > self.MAX_CNT: - # print("pullup by host 20-40us failed") - set_default_priority() - return None, "pullup by host 20-40us failed" + raise RuntimeError("pullup by host 20-40us failed") pulse_cnt = [0] * (2 * self.PULSES_CNT) - fix_crc = False - for i in range(0, self.PULSES_CNT * 2, 2): - while not GPIO.input(self.pin): - pulse_cnt[i] += 1 - if pulse_cnt[i] > self.MAX_CNT: - # print("pulldown by DHT timeout %d" % i)) - set_default_priority() - return None, "pulldown by DHT timeout {}".format(i) - - while GPIO.input(self.pin): - pulse_cnt[i + 1] += 1 - if pulse_cnt[i + 1] > self.MAX_CNT: - # print("pullup by DHT timeout {}".format((i + 1))) - if i == (self.PULSES_CNT - 1) * 2: - # fix_crc = True - # break + for pulse in range(0, self.PULSES_CNT * 2, 2): + while not GPIO.input(self._pin): + pulse_cnt[pulse] += 1 + if pulse_cnt[pulse] > self.MAX_CNT: + raise RuntimeError(f"pulldown by DHT timeout: {pulse}") + + while GPIO.input(self._pin): + pulse_cnt[pulse + 1] += 1 + if pulse_cnt[pulse + 1] > self.MAX_CNT: + if pulse == (self.PULSES_CNT - 1) * 2: pass - set_default_priority() - return None, "pullup by DHT timeout {}".format(i) - - # back to normal priority - set_default_priority() + raise RuntimeError(f"pullup by DHT timeout: {pulse}") total_cnt = 0 - for i in range(2, 2 * self.PULSES_CNT, 2): - total_cnt += pulse_cnt[i] + for pulse in range(2, 2 * self.PULSES_CNT, 2): + total_cnt += pulse_cnt[pulse] - # Low level ( 50 us) average counter + # Low level (50 us) average counter average_cnt = total_cnt / (self.PULSES_CNT - 1) - # print("low level average loop = {}".format(average_cnt)) data = "" - for i in range(3, 2 * self.PULSES_CNT, 2): - if pulse_cnt[i] > average_cnt: + for pulse in range(3, 2 * self.PULSES_CNT, 2): + if pulse_cnt[pulse] > average_cnt: data += "1" else: data += "0" - data0 = int(data[0:8], 2) - data1 = int(data[8:16], 2) - data2 = int(data[16:24], 2) - data3 = int(data[24:32], 2) - data4 = int(data[32:40], 2) - - if fix_crc and data4 != ((data0 + data1 + data2 + data3) & 0xFF): - data4 = data4 ^ 0x01 - data = data[0 : self.PULSES_CNT - 2] + ("1" if data4 & 0x01 else "0") - - if data4 == ((data0 + data1 + data2 + data3) & 0xFF): - if self._dht_type == DHTtype.DHT11: - humi = int(data0) - temp = int(data2) - elif self._dht_type == DHTtype.DHT22: - humi = float(int(data[0:16], 2) * 0.1) - temp = float(int(data[17:32], 2) * 0.2 * (0.5 - int(data[16], 2))) + byte0 = int(data[0:8], 2) + byte1 = int(data[8:16], 2) + byte2 = int(data[16:24], 2) + byte3 = int(data[24:32], 2) + crc_byte = int(data[32:40], 2) + + data_checksum = ((byte0 + byte1 + byte2 + byte3) & 0xFF) + if crc_byte != data_checksum: + raise RuntimeError("checksum error!") + + if self._dht_type == DHTType.DHT11: + humidity = byte0 + temperature = byte2 else: - # print("checksum error!") - return None, "checksum error!" - - return humi, temp - - def read(self, retries=15): - for i in range(retries): - humi, temp = self._read() - if humi is not None: - break - if humi is None: - return self._last_humi, self._last_temp - self._last_humi, self._last_temp = humi, temp - return humi, temp - - def measure(self): - """Performs a measurement and returns all available values in a dictionary. - The keys() are the names of the measurement and the values the corresponding values. - - :return: dict - """ + humidity = float(int(data[0:16], 2) * 0.1) + temperature = float(int(data[17:32], 2) * 0.2 * (0.5 - int(data[16], 2))) - humi, temp = self.read() - if humi == 0 and temp == 0: - return {"humidity": None, "temperature": None} - return {"humidity": float(humi), "temperature": float(temp)} + return humidity, temperature diff --git a/lib/py_edge_device/carlos/edge/device/io/device_metrics.py b/lib/py_edge_device/carlos/edge/device/io/device_metrics.py index 567a59f8..e70c1a30 100644 --- a/lib/py_edge_device/carlos/edge/device/io/device_metrics.py +++ b/lib/py_edge_device/carlos/edge/device/io/device_metrics.py @@ -1,6 +1,5 @@ import psutil from carlos.edge.interface.device import AnalogInput, IOConfig, peripheral_registry -from gpiozero import CPUTemperature class DeviceMetrics(AnalogInput): @@ -24,7 +23,8 @@ def read(self) -> dict[str, float]: def _read_cpu_temp() -> float: """Reads the CPU temperature.""" try: - return CPUTemperature().temperature + with open("/sys/class/thermal/thermal_zone0/temp") as f: + return float(f.read().strip()) / 1000 except FileNotFoundError: return 0.0 diff --git a/lib/py_edge_device/carlos/edge/device/io/dht11.py b/lib/py_edge_device/carlos/edge/device/io/dht11.py index e69de29b..ac387d31 100644 --- a/lib/py_edge_device/carlos/edge/device/io/dht11.py +++ b/lib/py_edge_device/carlos/edge/device/io/dht11.py @@ -0,0 +1,35 @@ +from carlos.edge.interface.device import AnalogInput, GPIOConfig, peripheral_registry + +from ._dhtxx import DHT, DHTType + + +class DHT11(AnalogInput): + """DHT11 Temperature and Humidity Sensor.""" + + def __init__(self, config: GPIOConfig): + + super().__init__(config=config) + + self._dht = DHT(dht_type=DHTType.DHT11, pin=config.pin) + + def read(self) -> dict[str, float]: + """Reads the temperature and humidity.""" + + # Reading the DHT sensor is quite unreliable, as the device is not a real-time + # device. Thus, we just try it a couple of times and fail if it does not work. + for i in range(16): + try: + temperature, humidity = self._dht.read() + return { + "temperature": temperature, + "humidity": humidity, + } + except RuntimeError: + pass + + raise RuntimeError("Could not read DHT11 sensor.") + + + + +peripheral_registry.register(ptype=__name__, config=GPIOConfig, factory=DHT11) \ No newline at end of file diff --git a/lib/py_edge_device/carlos/edge/device/protocol/__init__.py b/lib/py_edge_device/carlos/edge/device/protocol/__init__.py index e69de29b..35723ffe 100644 --- a/lib/py_edge_device/carlos/edge/device/protocol/__init__.py +++ b/lib/py_edge_device/carlos/edge/device/protocol/__init__.py @@ -0,0 +1,4 @@ +__all__ = ["GPIO", "I2C"] + +from .gpio import GPIO +from .i2c import I2C diff --git a/lib/py_edge_device/carlos/edge/device/protocol/gpio.py b/lib/py_edge_device/carlos/edge/device/protocol/gpio.py index 96b7e131..1969df7e 100644 --- a/lib/py_edge_device/carlos/edge/device/protocol/gpio.py +++ b/lib/py_edge_device/carlos/edge/device/protocol/gpio.py @@ -3,4 +3,4 @@ try: from RPi import GPIO except ImportError: - from RPiSim import GPIO + from RPiSim.GPIO import GPIO diff --git a/lib/py_edge_device/carlos/edge/device/protocol/i2c.py b/lib/py_edge_device/carlos/edge/device/protocol/i2c.py index 09d1a6de..9649726f 100644 --- a/lib/py_edge_device/carlos/edge/device/protocol/i2c.py +++ b/lib/py_edge_device/carlos/edge/device/protocol/i2c.py @@ -1,22 +1,10 @@ -#!/usr/bin/python - import re from threading import RLock from typing import Sequence import smbus2 - -class i2cLock: - """Use this i2c lock to prevent simultaneous access of the i2c bus by different - threads.""" - - instance = None - - def __new__(cls): - if not i2cLock.instance: - i2cLock.instance = RLock() - return i2cLock.instance +I2C_LOCK = RLock() class I2C: @@ -38,6 +26,11 @@ def __init__(self, address: int, bus: int | None = None): bus=bus if bus is not None else I2C.get_pi_i2v_bus_number() ) + @property + def lock(self) -> RLock: + """Returns the i2c lock""" + return I2C_LOCK + def write8(self, register: int, value: int): """Writes an 8-bit value to the specified register/address""" try: From 3ae3cd221a932bfe6823b0845a3d26c76d9158d3 Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 01:37:43 +0200 Subject: [PATCH 08/57] add si1145 sensor --- .../carlos/edge/device/config.py | 1 + .../carlos/edge/device/io/_dhtxx.py | 2 +- .../carlos/edge/device/io/dht11.py | 4 +- .../carlos/edge/device/io/si1145.py | 485 ++++++++++++++++++ .../carlos/edge/device/protocol/__init__.py | 4 +- .../carlos/edge/device/protocol/i2c.py | 11 +- 6 files changed, 499 insertions(+), 8 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/config.py b/lib/py_edge_device/carlos/edge/device/config.py index 67c3abdc..e99c4b1e 100644 --- a/lib/py_edge_device/carlos/edge/device/config.py +++ b/lib/py_edge_device/carlos/edge/device/config.py @@ -12,6 +12,7 @@ from typing import TypeVar import yaml +from carlos.edge.interface.device import DeviceConfig from pydantic import BaseModel from carlos.edge.device.constants import CONFIG_FILE_NAME diff --git a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py index dc434bfd..1c7dfbb9 100644 --- a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py +++ b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py @@ -98,7 +98,7 @@ def read(self) -> tuple[int | float, int | float]: byte3 = int(data[24:32], 2) crc_byte = int(data[32:40], 2) - data_checksum = ((byte0 + byte1 + byte2 + byte3) & 0xFF) + data_checksum = (byte0 + byte1 + byte2 + byte3) & 0xFF if crc_byte != data_checksum: raise RuntimeError("checksum error!") diff --git a/lib/py_edge_device/carlos/edge/device/io/dht11.py b/lib/py_edge_device/carlos/edge/device/io/dht11.py index ac387d31..037d0809 100644 --- a/lib/py_edge_device/carlos/edge/device/io/dht11.py +++ b/lib/py_edge_device/carlos/edge/device/io/dht11.py @@ -30,6 +30,4 @@ def read(self) -> dict[str, float]: raise RuntimeError("Could not read DHT11 sensor.") - - -peripheral_registry.register(ptype=__name__, config=GPIOConfig, factory=DHT11) \ No newline at end of file +peripheral_registry.register(ptype=__name__, config=GPIOConfig, factory=DHT11) diff --git a/lib/py_edge_device/carlos/edge/device/io/si1145.py b/lib/py_edge_device/carlos/edge/device/io/si1145.py index e69de29b..126718d4 100644 --- a/lib/py_edge_device/carlos/edge/device/io/si1145.py +++ b/lib/py_edge_device/carlos/edge/device/io/si1145.py @@ -0,0 +1,485 @@ +import time + +from carlos.edge.interface.device import AnalogInput, I2CConfig, peripheral_registry + +from carlos.edge.device.protocol import I2C + + +class SI1145(AnalogInput): + + def __init__(self, config: I2CConfig): + + if config.address != SDL_Pi_SI1145.ADDR: + raise ValueError("The address of the SI1145 sensor must be 0x60.") + + super().__init__(config=config) + + self._si1145 = SDL_Pi_SI1145() + + def read(self) -> dict[str, float]: + """Reads various light levels from the sensor.""" + + vis_raw = self._si1145.read_visible() + vis_lux = self._si1145.convert_visible_to_lux(vis_raw) + ir_raw = self._si1145.read_ir() + ir_lux = self._si1145.convert_ir_to_lux(ir_raw) + uv_idx = self._si1145.read_uv_index() + + return { + "visual-light-raw": float(vis_raw), + "visual-light": float(vis_lux), + "infrared-light-raw": float(ir_raw), + "infrared-light": float(ir_lux), + "uv-index": float(uv_idx), + } + + +peripheral_registry.register(ptype=__name__, config=I2CConfig, factory=SI1145) + + +class SDL_Pi_SI1145: + """Interface to the SI1145 UV Light Sensor by Adafruit. + + Note: This sensor has a static I2C Address of 0x60. + + https://www.silabs.com/documents/public/data-sheets/Si1145-46-47.pdf + + Modified for medium range vis, IR SDL December 2016 and + non Adafruit I2C (interfers with others) + + Original Author: Joe Gutting + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + """ + + # COMMANDS + PARAM_QUERY = 0x80 + PARAM_SET = 0xA0 + NOP = 0x0 + RESET = 0x01 + BUSADDR = 0x02 + PS_FORCE = 0x05 + ALS_FORCE = 0x06 + PSALS_FORCE = 0x07 + PS_PAUSE = 0x09 + ALS_PAUSE = 0x0A + PSALS_PAUSE = 0xB + PS_AUTO = 0x0D + ALS_AUTO = 0x0E + PSALS_AUTO = 0x0F + GET_CAL = 0x12 + + # Parameters + PARAM_I2CADDR = 0x00 + PARAM_CHLIST = 0x01 + PARAM_CHLIST_ENUV = 0x80 + PARAM_CHLIST_ENAUX = 0x40 + PARAM_CHLIST_ENALSIR = 0x20 + PARAM_CHLIST_ENALSVIS = 0x10 + PARAM_CHLIST_ENPS1 = 0x01 + PARAM_CHLIST_ENPS2 = 0x02 + PARAM_CHLIST_ENPS3 = 0x04 + + PARAM_PSLED12SEL = 0x02 + PARAM_PSLED12SEL_PS2NONE = 0x00 + PARAM_PSLED12SEL_PS2LED1 = 0x10 + PARAM_PSLED12SEL_PS2LED2 = 0x20 + PARAM_PSLED12SEL_PS2LED3 = 0x40 + PARAM_PSLED12SEL_PS1NONE = 0x00 + PARAM_PSLED12SEL_PS1LED1 = 0x01 + PARAM_PSLED12SEL_PS1LED2 = 0x02 + PARAM_PSLED12SEL_PS1LED3 = 0x04 + + PARAM_PSLED3SEL = 0x03 + PARAM_PSENCODE = 0x05 + PARAM_ALSENCODE = 0x06 + + PARAM_PS1ADCMUX = 0x07 + PARAM_PS2ADCMUX = 0x08 + PARAM_PS3ADCMUX = 0x09 + PARAM_PSADCOUNTER = 0x0A + PARAM_PSADCGAIN = 0x0B + PARAM_PSADCMISC = 0x0C + PARAM_PSADCMISC_RANGE = 0x20 + PARAM_PSADCMISC_PSMODE = 0x04 + + PARAM_ALSIRADCMUX = 0x0E + PARAM_AUXADCMUX = 0x0F + + PARAM_ALS_VIS_ADC_COUNTER = 0x10 + PARAM_ALS_VIS_ADC_GAIN = 0x11 + PARAM_ALS_VIS_ADC_MISC = 0x12 + PARAM_ALS_VIS_ADC_MISC_VISRANGE = 0x10 + # PARAM_ALS_VIS_ADC_MISC_VISRANGE = 0x00 + + PARAM_ALS_IR_ADC_COUNTER = 0x1D + PARAM_ALS_IR_ADC_GAIN = 0x1E + PARAM_ALS_IR_ADC_MISC = 0x1F + PARAM_ALS_IR_ADC_MISC_RANGE = 0x20 + # PARAM_ALS_IR_ADC_MISC_RANGE = 0x00 + + PARAM_ADCCOUNTER_511CLK = 0x70 + + PARAM_ADCMUX_SMALLIR = 0x00 + PARAM_ADCMUX_LARGEIR = 0x03 + + # REGISTERS + REG_PARTID = 0x00 + REG_REVID = 0x01 + REG_SEQID = 0x02 + + REG_INTCFG = 0x03 + REG_INTCFG_INTOE = 0x01 + REG_INTCFG_INTMODE = 0x02 + + REG_IRQEN = 0x04 + REG_IRQEN_ALSEVERYSAMPLE = 0x01 + REG_IRQEN_PS1EVERYSAMPLE = 0x04 + REG_IRQEN_PS2EVERYSAMPLE = 0x08 + REG_IRQEN_PS3EVERYSAMPLE = 0x10 + + REG_IRQMODE1 = 0x05 + REG_IRQMODE2 = 0x06 + + REG_HWKEY = 0x07 + REG_MEASRATE0 = 0x08 + REG_MEASRATE1 = 0x09 + REG_PSRATE = 0x0A + REG_PSLED21 = 0x0F + REG_PSLED3 = 0x10 + REG_UCOEFF0 = 0x13 + REG_UCOEFF1 = 0x14 + REG_UCOEFF2 = 0x15 + REG_UCOEFF3 = 0x16 + REG_PARAMWR = 0x17 + REG_COMMAND = 0x18 + REG_RESPONSE = 0x20 + REG_IRQSTAT = 0x21 + REG_IRQSTAT_ALS = 0x01 + + REG_ALSVISDATA0 = 0x22 + REG_ALSVISDATA1 = 0x23 + REG_ALSIRDATA0 = 0x24 + REG_ALSIRDATA1 = 0x25 + REG_PS1DATA0 = 0x26 + REG_PS1DATA1 = 0x27 + REG_PS2DATA0 = 0x28 + REG_PS2DATA1 = 0x29 + REG_PS3DATA0 = 0x2A + REG_PS3DATA1 = 0x2B + REG_UVINDEX0 = 0x2C + REG_UVINDEX1 = 0x2D + REG_PARAMRD = 0x2E + REG_CHIPSTAT = 0x30 + + # I2C Address + ADDR = 0x60 + + DARK_OFFSET_VIS = 259 + DARK_OFFSE_TIR = 253 + + def __init__(self): + """Constructor.""" + + self._i2c = I2C(address=SDL_Pi_SI1145.ADDR) + + self._reset() + self._load_calibration() + + # device reset + def _reset(self): + """Resets the device + + :return: + """ + + with self._i2c.lock: + self._i2c.write8(register=SDL_Pi_SI1145.REG_MEASRATE0, value=0x00) + self._i2c.write8(register=SDL_Pi_SI1145.REG_MEASRATE1, value=0x00) + self._i2c.write8(register=SDL_Pi_SI1145.REG_IRQEN, value=0x00) + self._i2c.write8(register=SDL_Pi_SI1145.REG_IRQMODE1, value=0x00) + self._i2c.write8(register=SDL_Pi_SI1145.REG_IRQMODE2, value=0x00) + self._i2c.write8(register=SDL_Pi_SI1145.REG_INTCFG, value=0x00) + self._i2c.write8(register=SDL_Pi_SI1145.REG_IRQSTAT, value=0xFF) + + self._i2c.write8( + register=SDL_Pi_SI1145.REG_COMMAND, value=SDL_Pi_SI1145.RESET + ) + time.sleep(0.01) + self._i2c.write8(register=SDL_Pi_SI1145.REG_HWKEY, value=0x17) + time.sleep(0.01) + + def write_param(self, parameter: int, value: int) -> int: + """Write Parameter to the Sensor.""" + + with self._i2c.lock: + self._i2c.write8(register=SDL_Pi_SI1145.REG_PARAMWR, value=value) + self._i2c.write8( + register=SDL_Pi_SI1145.REG_COMMAND, + value=parameter | SDL_Pi_SI1145.PARAM_SET, + ) + param_val = self._i2c.read_uint8(register=SDL_Pi_SI1145.REG_PARAMRD) + return param_val + + def read_param(self, parameter: int) -> int: + """Read Parameter from the Sensor.""" + + with self._i2c.lock: + self._i2c.write8( + register=SDL_Pi_SI1145.REG_COMMAND, + value=parameter | SDL_Pi_SI1145.PARAM_QUERY, + ) + return self._i2c.read_uint8(register=SDL_Pi_SI1145.REG_PARAMRD) + + # load calibration to sensor + def _load_calibration(self): + """Load calibration data.""" + + with self._i2c.lock: + # Enable UVindex measurement coefficients! + self._i2c.write8(register=SDL_Pi_SI1145.REG_UCOEFF0, value=0x29) + self._i2c.write8(register=SDL_Pi_SI1145.REG_UCOEFF1, value=0x89) + self._i2c.write8(register=SDL_Pi_SI1145.REG_UCOEFF2, value=0x02) + self._i2c.write8(register=SDL_Pi_SI1145.REG_UCOEFF3, value=0x00) + + # Enable UV sensor + self.write_param( + parameter=SDL_Pi_SI1145.PARAM_CHLIST, + value=SDL_Pi_SI1145.PARAM_CHLIST_ENUV + | SDL_Pi_SI1145.PARAM_CHLIST_ENALSIR + | SDL_Pi_SI1145.PARAM_CHLIST_ENALSVIS + | SDL_Pi_SI1145.PARAM_CHLIST_ENPS1, + ) + + # Enable interrupt on every sample + self._i2c.write8( + register=SDL_Pi_SI1145.REG_INTCFG, + value=SDL_Pi_SI1145.REG_INTCFG_INTOE, + ) + self._i2c.write8( + register=SDL_Pi_SI1145.REG_IRQEN, + value=SDL_Pi_SI1145.REG_IRQEN_ALSEVERYSAMPLE, + ) + + # /****************************** Prox Sense 1 */ + + # Program LED current + self._i2c.write8( + register=SDL_Pi_SI1145.REG_PSLED21, value=0x03 + ) # 20mA for LED 1 only + self.write_param( + parameter=SDL_Pi_SI1145.PARAM_PS1ADCMUX, + value=SDL_Pi_SI1145.PARAM_ADCMUX_LARGEIR, + ) + + # Prox sensor #1 uses LED #1 + self.write_param( + parameter=SDL_Pi_SI1145.PARAM_PSLED12SEL, + value=SDL_Pi_SI1145.PARAM_PSLED12SEL_PS1LED1, + ) + + # Fastest clocks, clock div 1 + self.write_param(parameter=SDL_Pi_SI1145.PARAM_PSADCGAIN, value=0x00) + + # Take 511 clocks to measure + self.write_param( + parameter=SDL_Pi_SI1145.PARAM_PSADCOUNTER, + value=SDL_Pi_SI1145.PARAM_ADCCOUNTER_511CLK, + ) + + # in prox mode, high range + self.write_param( + parameter=SDL_Pi_SI1145.PARAM_PSADCMISC, + value=SDL_Pi_SI1145.PARAM_PSADCMISC_RANGE + | SDL_Pi_SI1145.PARAM_PSADCMISC_PSMODE, + ) + self.write_param( + parameter=SDL_Pi_SI1145.PARAM_ALSIRADCMUX, + value=SDL_Pi_SI1145.PARAM_ADCMUX_SMALLIR, + ) + + # Fastest clocks, clock div 1 + self.write_param( + parameter=SDL_Pi_SI1145.PARAM_ALS_IR_ADC_GAIN, + value=0, + # value=4 + ) + + # Take 511 clocks to measure + self.write_param( + parameter=SDL_Pi_SI1145.PARAM_ALS_IR_ADC_COUNTER, + value=SDL_Pi_SI1145.PARAM_ADCCOUNTER_511CLK, + ) + + # in high range mode + self.write_param( + parameter=SDL_Pi_SI1145.PARAM_ALS_IR_ADC_MISC, + value=0, + # value=SDL_Pi_SI1145.PARAM_ALS_IR_ADC_MISC_RANGE + ) + + # fastest clocks, clock div 1 + self.write_param( + parameter=SDL_Pi_SI1145.PARAM_ALS_VIS_ADC_GAIN, + value=0, + # value=4 + ) + + # Take 511 clocks to measure + self.write_param( + parameter=SDL_Pi_SI1145.PARAM_ALS_VIS_ADC_COUNTER, + value=SDL_Pi_SI1145.PARAM_ADCCOUNTER_511CLK, + ) + + # in high range mode (not normal signal) + self.write_param( + parameter=SDL_Pi_SI1145.PARAM_ALS_VIS_ADC_MISC, + value=0, + # value=SDL_Pi_SI1145.PARAM_ALS_VIS_ADC_MISC_VISRANGE + ) + + # measurement rate for auto + self._i2c.write8( + register=SDL_Pi_SI1145.REG_MEASRATE0, value=0xFF + ) # 255 * 31.25uS = 8ms + + # auto run + self._i2c.write8( + register=SDL_Pi_SI1145.REG_COMMAND, value=SDL_Pi_SI1145.PSALS_AUTO + ) + + def read_visible(self) -> int: + """returns visible + IR light levels""" + + with self._i2c.lock: + return self._i2c.read_uint16(register=SDL_Pi_SI1145.REG_ALSVISDATA0) + + def read_visible_lux(self) -> float: + """returns visible + IR light levels in lux""" + + self.convert_visible_to_lux(self.read_visible()) + + def read_ir(self) -> int: + """returns IR light levels""" + + with self._i2c.lock: + return self._i2c.read_uint16(register=SDL_Pi_SI1145.REG_ALSIRDATA0) + + def read_ir_lux(self) -> float: + """returns IR light levels in lux""" + + return self.convert_ir_to_lux(self.read_ir()) + + def read_prox(self) -> int: + """Returns "Proximity" - assumes an IR LED is attached to LED""" + + with self._i2c.lock: + return self._i2c.read_uint16(register=SDL_Pi_SI1145.REG_PS1DATA0) + + def read_uv(self) -> int: + """Returns the UV index * 100 (divide by 100 to get the index)""" + + with self._i2c.lock: + # apply additional calibration of /10 based on sunlight + return self._i2c.read_uint16(register=SDL_Pi_SI1145.REG_UVINDEX0) / 10 + + def read_uv_index(self) -> float: + """Returns the UV Index.""" + + return self.read_uv() / 100 + + @staticmethod + def convert_ir_to_lux(ir: int) -> float: + """Converts IR levels to lux.""" + + return SDL_Pi_SI1145._convert_raw_to_lux( + raw=ir, + dark_offset=SDL_Pi_SI1145.DARK_OFFSE_TIR, + calibration_factor=50, # calibration factor to sunlight applied + param_adc_gain=SDL_Pi_SI1145.PARAM_ALS_IR_ADC_GAIN, + param_adc_misc=SDL_Pi_SI1145.PARAM_ALS_IR_ADC_MISC, + ) + + @staticmethod + def convert_uv_to_index(uv: int) -> float: + """Converts the read UV values to UV index.""" + + return uv / 100 + + @staticmethod + def convert_visible_to_lux(vis: int) -> float: + """Converts the visible light level to lux.""" + + # Param 1: ALS_VIS_ADC_MISC + return SDL_Pi_SI1145._convert_raw_to_lux( + raw=vis, + dark_offset=SDL_Pi_SI1145.DARK_OFFSET_VIS, + calibration_factor=100, # calibration to bright sunlight added + param_adc_gain=SDL_Pi_SI1145.PARAM_ALS_VIS_ADC_GAIN, + param_adc_misc=SDL_Pi_SI1145.PARAM_ALS_VIS_ADC_MISC, + ) + + def _convert_raw_to_lux( + self, + raw: int, + dark_offset: int, + calibration_factor: int, + param_adc_gain: int, + param_adc_misc: int, + ) -> float: + """Converts a raw input to Lux by applying a dark offset and a calibration + factor.""" + + raw = raw - dark_offset + if raw < 0: + raw = 0 + + lux = 2.44 + + # Get gain + gain = 1 + # These are set to defaults in the Adafruit driver - + # need to change if you change them in the SI1145 driver + # range_ = self.read_param(parameter=adc_misc) + # if (range_ & 32) == 32: + # gain = 14.5 + + # Get sensitivity + multiplier = 1 + # These are set to defaults in the Adafruit driver - + # need to change if you change them in the SI1145 driver + # sensitivity = self.read_param(parameter=adc_gain) + # if (sensitivity & 7) == 0: + # multiplier = 1 + # if (sensitivity & 7) == 1: + # multiplier = 2 + # if (sensitivity & 7) == 2: + # multiplier = 4 + # if (sensitivity & 7) == 3: + # multiplier = 8 + # if (sensitivity & 7) == 4: + # multiplier = 16 + # if (sensitivity & 7) == 5: + # multiplier = 32 + # if (sensitivity & 7) == 6: + # multiplier = 64 + # if (sensitivity & 7) == 7: + # multiplier = 128 + + return raw * (gain / (lux * multiplier)) * calibration_factor diff --git a/lib/py_edge_device/carlos/edge/device/protocol/__init__.py b/lib/py_edge_device/carlos/edge/device/protocol/__init__.py index 35723ffe..8f3cbe0a 100644 --- a/lib/py_edge_device/carlos/edge/device/protocol/__init__.py +++ b/lib/py_edge_device/carlos/edge/device/protocol/__init__.py @@ -1,4 +1,4 @@ -__all__ = ["GPIO", "I2C"] +__all__ = ["GPIO", "I2C", "I2cLock"] from .gpio import GPIO -from .i2c import I2C +from .i2c import I2C, I2cLock diff --git a/lib/py_edge_device/carlos/edge/device/protocol/i2c.py b/lib/py_edge_device/carlos/edge/device/protocol/i2c.py index 9649726f..3688284a 100644 --- a/lib/py_edge_device/carlos/edge/device/protocol/i2c.py +++ b/lib/py_edge_device/carlos/edge/device/protocol/i2c.py @@ -1,3 +1,5 @@ +__all__ = ["I2cLock", "I2C"] + import re from threading import RLock from typing import Sequence @@ -7,6 +9,11 @@ I2C_LOCK = RLock() +class I2cLock: + def __new__(cls): + return I2C_LOCK + + class I2C: """This class is based on, but heavily modified from, the Adafruit_I2C class""" @@ -84,7 +91,7 @@ def read_list(self, register: int, length: int): f"Error accessing 0x{self.address:0x}: Check your I2C address." ) - def read_uint8(self, register: int): + def read_uint8(self, register: int) -> int: """Read an unsigned byte from the I2C device""" try: return self.bus.read_byte_data(i2c_addr=self.address, register=register) @@ -142,7 +149,7 @@ def get_pi_revision() -> int: # Couldn't find the revision, assume revision 0 like older code for # compatibility. return 0 - except: + except Exception: return 0 @staticmethod From 0bd92abef648586fca610e9caa40b449ba627fab Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 01:48:56 +0200 Subject: [PATCH 09/57] add relay io --- .../carlos/edge/device/io/_dhtxx.py | 3 --- .../carlos/edge/device/io/relay.py | 19 +++++++++++++++++++ .../carlos/edge/device/protocol/gpio.py | 4 ++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py index 1c7dfbb9..b2e3be05 100644 --- a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py +++ b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py @@ -31,9 +31,6 @@ def __init__(self, dht_type: DHTType, pin: int): self._pin = pin self._dht_type = dht_type - # setup the GPIO mode - GPIO.setmode(GPIO.BCM) - GPIO.setwarnings(False) GPIO.setup(self._pin, GPIO.OUT) def read(self) -> tuple[int | float, int | float]: diff --git a/lib/py_edge_device/carlos/edge/device/io/relay.py b/lib/py_edge_device/carlos/edge/device/io/relay.py index e69de29b..02f7997d 100644 --- a/lib/py_edge_device/carlos/edge/device/io/relay.py +++ b/lib/py_edge_device/carlos/edge/device/io/relay.py @@ -0,0 +1,19 @@ +from carlos.edge.interface.device import DigitalOutput, GPIOConfig, peripheral_registry + +from carlos.edge.device.protocol import GPIO + + +class Relay(DigitalOutput): + """Relay.""" + + def __init__(self, config: GPIOConfig): + super().__init__(config=config) + + GPIO.setup(self.config.pin, GPIO.OUT) + + def write(self, value: bool): + """Writes the value to the relay.""" + GPIO.output(self.config.pin, value) + + +peripheral_registry.register(ptype=__name__, config=GPIOConfig, factory=Relay) diff --git a/lib/py_edge_device/carlos/edge/device/protocol/gpio.py b/lib/py_edge_device/carlos/edge/device/protocol/gpio.py index 1969df7e..07cd5942 100644 --- a/lib/py_edge_device/carlos/edge/device/protocol/gpio.py +++ b/lib/py_edge_device/carlos/edge/device/protocol/gpio.py @@ -4,3 +4,7 @@ from RPi import GPIO except ImportError: from RPiSim.GPIO import GPIO + +# Choose the GPIO mode globally +GPIO.setmode(GPIO.BCM) +GPIO.setwarnings(False) From 16058982e1b90d136717f65b99563391b8da422a Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 01:49:22 +0200 Subject: [PATCH 10/57] add relay io --- lib/py_carlos_database/.idea/py_carlos_database.iml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 lib/py_carlos_database/.idea/py_carlos_database.iml diff --git a/lib/py_carlos_database/.idea/py_carlos_database.iml b/lib/py_carlos_database/.idea/py_carlos_database.iml new file mode 100644 index 00000000..25e55379 --- /dev/null +++ b/lib/py_carlos_database/.idea/py_carlos_database.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file From f5fa31a7557ad5753f965b2a40328f9114437841 Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 02:00:30 +0200 Subject: [PATCH 11/57] fix mypy issues --- lib/py_edge_device/carlos/edge/device/io/_dhtxx.py | 6 +++--- lib/py_edge_device/carlos/edge/device/io/si1145.py | 12 +++++------- .../carlos/edge/device/protocol/gpio.py | 4 ++-- lib/py_edge_device/poetry.lock | 13 ++++++++++++- lib/py_edge_device/pyproject.toml | 1 + .../carlos/edge/interface/device/peripheral.py | 11 +++++++---- 6 files changed, 30 insertions(+), 17 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py index b2e3be05..5beb947c 100644 --- a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py +++ b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py @@ -33,7 +33,7 @@ def __init__(self, dht_type: DHTType, pin: int): GPIO.setup(self._pin, GPIO.OUT) - def read(self) -> tuple[int | float, int | float]: + def read(self) -> tuple[float, float]: """Internal read method. http://www.ocfreaks.com/basics-interfacing-dht11-dht22-humidity-temperature-sensor-mcu/ @@ -100,8 +100,8 @@ def read(self) -> tuple[int | float, int | float]: raise RuntimeError("checksum error!") if self._dht_type == DHTType.DHT11: - humidity = byte0 - temperature = byte2 + humidity = float(byte0) + temperature = float(byte2) else: humidity = float(int(data[0:16], 2) * 0.1) temperature = float(int(data[17:32], 2) * 0.2 * (0.5 - int(data[16], 2))) diff --git a/lib/py_edge_device/carlos/edge/device/io/si1145.py b/lib/py_edge_device/carlos/edge/device/io/si1145.py index 126718d4..3804c414 100644 --- a/lib/py_edge_device/carlos/edge/device/io/si1145.py +++ b/lib/py_edge_device/carlos/edge/device/io/si1145.py @@ -373,7 +373,7 @@ def read_visible(self) -> int: def read_visible_lux(self) -> float: """returns visible + IR light levels in lux""" - self.convert_visible_to_lux(self.read_visible()) + return self.convert_visible_to_lux(self.read_visible()) def read_ir(self) -> int: """returns IR light levels""" @@ -404,11 +404,10 @@ def read_uv_index(self) -> float: return self.read_uv() / 100 - @staticmethod - def convert_ir_to_lux(ir: int) -> float: + def convert_ir_to_lux(self, ir: int) -> float: """Converts IR levels to lux.""" - return SDL_Pi_SI1145._convert_raw_to_lux( + return self._convert_raw_to_lux( raw=ir, dark_offset=SDL_Pi_SI1145.DARK_OFFSE_TIR, calibration_factor=50, # calibration factor to sunlight applied @@ -422,12 +421,11 @@ def convert_uv_to_index(uv: int) -> float: return uv / 100 - @staticmethod - def convert_visible_to_lux(vis: int) -> float: + def convert_visible_to_lux(self, vis: int) -> float: """Converts the visible light level to lux.""" # Param 1: ALS_VIS_ADC_MISC - return SDL_Pi_SI1145._convert_raw_to_lux( + return self._convert_raw_to_lux( raw=vis, dark_offset=SDL_Pi_SI1145.DARK_OFFSET_VIS, calibration_factor=100, # calibration to bright sunlight added diff --git a/lib/py_edge_device/carlos/edge/device/protocol/gpio.py b/lib/py_edge_device/carlos/edge/device/protocol/gpio.py index 07cd5942..1e341a2c 100644 --- a/lib/py_edge_device/carlos/edge/device/protocol/gpio.py +++ b/lib/py_edge_device/carlos/edge/device/protocol/gpio.py @@ -1,9 +1,9 @@ __all__ = ["GPIO"] try: - from RPi import GPIO + from RPi import GPIO # type: ignore except ImportError: - from RPiSim.GPIO import GPIO + from RPiSim.GPIO import GPIO # type: ignore # Choose the GPIO mode globally GPIO.setmode(GPIO.BCM) diff --git a/lib/py_edge_device/poetry.lock b/lib/py_edge_device/poetry.lock index 78eb4e7b..be302a1a 100644 --- a/lib/py_edge_device/poetry.lock +++ b/lib/py_edge_device/poetry.lock @@ -1159,6 +1159,17 @@ files = [ [package.extras] doc = ["reno", "sphinx", "tornado (>=4.5)"] +[[package]] +name = "types-psutil" +version = "5.9.5.20240316" +description = "Typing stubs for psutil" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-psutil-5.9.5.20240316.tar.gz", hash = "sha256:5636f5714bb930c64bb34c4d47a59dc92f9d610b778b5364a31daa5584944848"}, + {file = "types_psutil-5.9.5.20240316-py3-none-any.whl", hash = "sha256:2fdd64ea6e97befa546938f486732624f9255fde198b55e6f00fda236f059f64"}, +] + [[package]] name = "types-pyyaml" version = "6.0.12.20240311" @@ -1259,4 +1270,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.12" -content-hash = "166a5a92157a481abe4e251803a12482d296a956307b2308c9e9d61e28e651af" +content-hash = "ec32e3ddd34a892e48827ce6d04bcde34f28a77371d3e6a53ed6ae37a03ccafc" diff --git a/lib/py_edge_device/pyproject.toml b/lib/py_edge_device/pyproject.toml index bdf9dae9..85cacf09 100644 --- a/lib/py_edge_device/pyproject.toml +++ b/lib/py_edge_device/pyproject.toml @@ -24,6 +24,7 @@ smbus2 = "^0.4.3" [tool.poetry.group.dev.dependencies] "devtools" = {path = "../py_dev_dependencies"} types-pyyaml = "^6.0.12.20240311" +types-psutil = "^5.9.5.20240316" [build-system] diff --git a/lib/py_edge_interface/carlos/edge/interface/device/peripheral.py b/lib/py_edge_interface/carlos/edge/interface/device/peripheral.py index ec0cc243..dd0a97e0 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/peripheral.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/peripheral.py @@ -2,15 +2,18 @@ import asyncio import concurrent.futures from abc import ABC, abstractmethod +from typing import TypeVar, Generic -from carlos.edge.interface.device.config import PeripheralConfig +from carlos.edge.interface.device.config import GPIOConfig, I2CConfig, IOConfig +Config = TypeVar("Config", I2CConfig, GPIOConfig, IOConfig) -class CarlosPeripheral(ABC): + +class CarlosPeripheral(ABC, Generic[Config]): """Common base class for all peripherals.""" - def __init__(self, config: PeripheralConfig): - self.config = config + def __init__(self, config: Config): + self.config: Config = config class AnalogInput(CarlosPeripheral, ABC): From 4fbceee4ab4176369acc5e5bd32a55003e496030 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Apr 2024 00:01:21 +0000 Subject: [PATCH 12/57] update lib/py_edge_interface Co-authored-by: flxdot --- .../carlos/edge/interface/device/peripheral.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/peripheral.py b/lib/py_edge_interface/carlos/edge/interface/device/peripheral.py index dd0a97e0..f1b62ea6 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/peripheral.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/peripheral.py @@ -2,7 +2,7 @@ import asyncio import concurrent.futures from abc import ABC, abstractmethod -from typing import TypeVar, Generic +from typing import Generic, TypeVar from carlos.edge.interface.device.config import GPIOConfig, I2CConfig, IOConfig From 24106a9b0f3b9bf4c80222981545dc51a7f8423c Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 18:27:50 +0200 Subject: [PATCH 13/57] interface typing --- .../carlos/edge/interface/device/__init__.py | 15 +- .../carlos/edge/interface/device/config.py | 85 ++-------- .../carlos/edge/interface/device/io.py | 157 +++++++++++++++--- .../edge/interface/device/peripheral.py | 46 ----- 4 files changed, 153 insertions(+), 150 deletions(-) delete mode 100644 lib/py_edge_interface/carlos/edge/interface/device/peripheral.py diff --git a/lib/py_edge_interface/carlos/edge/interface/device/__init__.py b/lib/py_edge_interface/carlos/edge/interface/device/__init__.py index 116f70cf..cd4a4635 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/__init__.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/__init__.py @@ -1,15 +1,12 @@ __all__ = [ "AnalogInput", "CarlosIO", - "DeviceConfig", "DigitalOutput", - "GPIOConfig", - "I2CConfig", - "IOConfig", - "PeripheralConfig", - "peripheral_registry", + "GpioConfig", + "I2cConfig", + "IoConfig", + "IoFactory", ] -from .config import DeviceConfig, GPIOConfig, I2CConfig, IOConfig, PeripheralConfig -from .io import peripheral_registry -from .peripheral import AnalogInput, CarlosIO, DigitalOutput +from .config import GpioConfig, I2cConfig, IoConfig +from .io import AnalogInput, CarlosIO, DigitalOutput, IoFactory diff --git a/lib/py_edge_interface/carlos/edge/interface/device/config.py b/lib/py_edge_interface/carlos/edge/interface/device/config.py index 350dbe57..9bec0b19 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/config.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/config.py @@ -1,16 +1,20 @@ -__all__ = ["DeviceConfig", "GPIOConfig", "I2CConfig", "PeripheralConfig"] +__all__ = ["GpioConfig", "I2cConfig", "IoConfig", "IoPtypeDict"] from abc import ABC -from typing import Literal +from typing import Literal, TypedDict -from pydantic import BaseModel, Field, field_validator, model_validator +from pydantic import BaseModel, Field, field_validator # Pin layout # 5V, 5V, GND, 14, 15, 18, GND, 23, 24, GND, 25, 8, 7, ID EEPROM, GND, 12, GND, 16, 20, 21 # Outer pins # noqa # 3.3V, 2, 3, 4, GND, 17, 27, 22, 3.3V, 10, 9, 11, GND, ID EEPROM, 5, 6, 13, 19, 26, GND # Inner pins # noqa -class IOConfig(BaseModel, ABC): +class IoPtypeDict(TypedDict): + ptype: str + + +class IoConfig(BaseModel, ABC): """Common base class for all IO configurations.""" identifier: str = Field( @@ -30,7 +34,7 @@ class IOConfig(BaseModel, ABC): ) -class GPIOConfig(IOConfig): +class GpioConfig(IoConfig): """Defines a single input configuration.""" protocol: Literal["gpio"] = Field( @@ -68,7 +72,7 @@ class GPIOConfig(IOConfig): ] = Field(..., description="The GPIO pin number.") -class DigitalGPIOOutputConfig(GPIOConfig): +class DigitalGpioOutputConfig(GpioConfig): """Defines a single digital output configuration.""" direction: Literal["output"] = Field( @@ -76,11 +80,7 @@ class DigitalGPIOOutputConfig(GPIOConfig): ) -I2C_PINS = [2, 3] -"""The Pin numbers designated for I2C communication.""" - - -class I2CConfig(BaseModel): +class I2cConfig(BaseModel): """Defines a single input configuration.""" protocol: Literal["i2c"] = Field( @@ -109,66 +109,3 @@ def validate_address(cls, value): raise ValueError("The valid I2C address range is 0x03 to 0x77.") return hex(value) - - -PeripheralConfig = GPIOConfig | I2CConfig | IOConfig - - -class DeviceConfig(BaseModel): - """Configures the pure device settings.""" - - io: list[PeripheralConfig] = Field( - default_factory=list, description="A list of IO configurations." - ) - - @model_validator(mode="after") - def _validate_address_or_pin_overlap(self): - """This function ensures that the configured pins and addresses are unique.""" - gpio_configs = [io for io in self.io if isinstance(io, GPIOConfig)] - - # Ensure GPIO pins are unique - seen_pins = set() - duplicate_gpio_pins = [ - gpio.pin - for gpio in gpio_configs - if gpio.pin in seen_pins or seen_pins.add(gpio.pin) - ] - if duplicate_gpio_pins: - raise ValueError( - f"The GPIO pins {duplicate_gpio_pins} are configured more than once." - f"Please ensure that each GPIO pin is configured only once." - ) - - i2c_configs = [io for io in self.io if isinstance(io, I2CConfig)] - if i2c_configs: - if any(gpio.pin in I2C_PINS for gpio in gpio_configs): - raise ValueError( - "The GPIO pins 2 and 3 are reserved for I2C communication." - "Please use other pins for GPIO configuration." - ) - - # Ensure I2C addresses are unique - seen_addresses = set() - duplicate_i2c_addresses = [ - i2c.address - for i2c in i2c_configs - if i2c.address in i2c_configs or seen_addresses.add(i2c.address) - ] - if duplicate_i2c_addresses: - raise ValueError( - f"The I2C addresses {duplicate_i2c_addresses} are configured more than " - f"once. Please ensure that each I2C address is configured only once." - ) - - # Ensure all identifiers are unique - seen_identifiers = set() - duplicate_identifiers = [ - io.identifier - for io in self.io - if io.identifier in seen_identifiers or seen_identifiers.add(io.identifier) - ] - if duplicate_identifiers: - raise ValueError( - f"The identifiers {duplicate_identifiers} are configured more than " - f"once. Please ensure that each identifier is configured only once." - ) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/io.py b/lib/py_edge_interface/carlos/edge/interface/device/io.py index 8970f14d..eb6db0a8 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/io.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/io.py @@ -1,25 +1,79 @@ -__all__ = ["peripheral_registry"] - +__all__ = [ + "AnalogInput", + "DigitalOutput", + "CarlosIO", + "IoFactory", + "validate_device_address_space", +] +import asyncio +import concurrent.futures +from abc import ABC, abstractmethod from collections import namedtuple -from typing import Callable, TypedDict, TypeVar +from typing import Callable, Generic, Iterable, TypeVar + +from .config import GpioConfig, I2cConfig, IoConfig, IoPtypeDict + +IoConfigTypeVar = TypeVar("IoConfigTypeVar", bound=IoConfig) + + +class CarlosPeripheral(ABC, Generic[IoConfigTypeVar]): + """Common base class for all peripherals.""" + + def __init__(self, config: IoConfigTypeVar): + self.config: IoConfigTypeVar = config + + +class AnalogInput(CarlosPeripheral, ABC): + """Common base class for all analog input peripherals.""" + + @abstractmethod + def read(self) -> dict[str, float]: + """Reads the value of the analog input. The return value is a dictionary + containing the value of the analog input.""" + pass + + async def read_async(self) -> dict[str, float]: + """Reads the value of the analog input asynchronously. The return value is a dictionary + containing the value of the analog input.""" + + loop = asyncio.get_running_loop() + + with concurrent.futures.ThreadPoolExecutor() as pool: + return await loop.run_in_executor(executor=pool, func=self.read) + + +class DigitalOutput(CarlosPeripheral, ABC): + """Common base class for all digital output peripherals.""" + + @abstractmethod + def set(self, value: bool): + pass + -from .config import PeripheralConfig -from .peripheral import CarlosIO +CarlosIO = AnalogInput | DigitalOutput -C = TypeVar("C", bound=PeripheralConfig) +FactoryItem = namedtuple("FactoryItem", ["config", "factory"]) -RegistryItem = namedtuple("RegistryItem", ["config", "factory"]) +class IoFactory: + """A singleton factory for io peripherals.""" -class ConfigDict(TypedDict): - ptype: str + _instance = None + _ptype_to_io: dict[str, FactoryItem] = {} + def __new__(cls): + if cls._instance is None: + cls._instance = super(IoFactory, cls).__new__(cls) + cls._instance._ptype_to_io = {} -class PeripheralRegistry: - def __init__(self): - self._peripherals: dict[str, RegistryItem] = {} + return cls._instance - def register(self, ptype: str, config: type[C], factory: Callable[[C], CarlosIO]): + def register( + self, + ptype: str, + config: type[IoConfigTypeVar], + factory: Callable[[IoConfigTypeVar], CarlosIO], + ): """Registers a peripheral with the peripheral registry. :param ptype: The peripheral type. @@ -27,22 +81,83 @@ def register(self, ptype: str, config: type[C], factory: Callable[[C], CarlosIO] :param factory: The peripheral factory function. """ - if ptype in self._peripherals: - raise ValueError(f"The peripheral {ptype} is already registered.") + if ptype in self._ptype_to_io: + raise RuntimeError(f"The peripheral {ptype} is already registered.") - self._peripherals[ptype] = RegistryItem(config, factory) + self._ptype_to_io[ptype] = FactoryItem(config, factory) - def build(self, config: ConfigDict) -> CarlosIO: - """Builds a peripheral from the peripheral registry.""" + def build(self, config: IoPtypeDict) -> CarlosIO: + """Builds a IO object from its configuration.""" ptype = config["ptype"] - if type not in self._peripherals: + if type not in self._ptype_to_io: raise ValueError(f"The peripheral {ptype} is not registered.") - entry = self._peripherals[ptype] + entry = self._ptype_to_io[ptype] return entry.factory(entry.config.model_validate(config)) -peripheral_registry = PeripheralRegistry() +I2C_PINS = [2, 3] +"""The Pin numbers designated for I2C communication.""" + + +def validate_device_address_space(configs: Iterable[IoConfig]): + """This function ensures that the configured pins and addresses are unique. + + :param configs: The list of IO configurations. + :raises ValueError: If any of the pins or addresses are configured more than once. + If the GPIO pins 2 and 3 are when I2C communication is configured. + If any of the identifiers are configured more than once. + """ + gpio_configs: list[GpioConfig] = [ + io for io in configs if isinstance(io, GpioConfig) + ] + + # Ensure GPIO pins are unique + seen_pins = set() + duplicate_gpio_pins = [ + gpio.pin + for gpio in gpio_configs + if gpio.pin in seen_pins or seen_pins.add(gpio.pin) # type: ignore[func-returns-value] # noqa: E501 + ] + if duplicate_gpio_pins: + raise ValueError( + f"The GPIO pins {duplicate_gpio_pins} are configured more than once." + f"Please ensure that each GPIO pin is configured only once." + ) + + i2c_configs: list[I2cConfig] = [io for io in configs if isinstance(io, I2cConfig)] # type: ignore[misc] # noqa: E501 + if i2c_configs: + if any(gpio.pin in I2C_PINS for gpio in gpio_configs): + raise ValueError( + "The GPIO pins 2 and 3 are reserved for I2C communication." + "Please use other pins for GPIO configuration." + ) + + # Ensure I2C addresses are unique + seen_addresses = set() + duplicate_i2c_addresses = [ + i2c.address + for i2c in i2c_configs + if i2c.address in i2c_configs or seen_addresses.add(i2c.address) # type: ignore[func-returns-value] # noqa: E501 + ] + if duplicate_i2c_addresses: + raise ValueError( + f"The I2C addresses {duplicate_i2c_addresses} are configured more than " + f"once. Please ensure that each I2C address is configured only once." + ) + + # Ensure all identifiers are unique + seen_identifiers = set() + duplicate_identifiers = [ + io.identifier + for io in configs + if io.identifier in seen_identifiers or seen_identifiers.add(io.identifier) # type: ignore[func-returns-value] # noqa: E501 + ] + if duplicate_identifiers: + raise ValueError( + f"The identifiers {duplicate_identifiers} are configured more than " + f"once. Please ensure that each identifier is configured only once." + ) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/peripheral.py b/lib/py_edge_interface/carlos/edge/interface/device/peripheral.py deleted file mode 100644 index dd0a97e0..00000000 --- a/lib/py_edge_interface/carlos/edge/interface/device/peripheral.py +++ /dev/null @@ -1,46 +0,0 @@ -__all__ = ["AnalogInput", "DigitalOutput", "CarlosIO"] -import asyncio -import concurrent.futures -from abc import ABC, abstractmethod -from typing import TypeVar, Generic - -from carlos.edge.interface.device.config import GPIOConfig, I2CConfig, IOConfig - -Config = TypeVar("Config", I2CConfig, GPIOConfig, IOConfig) - - -class CarlosPeripheral(ABC, Generic[Config]): - """Common base class for all peripherals.""" - - def __init__(self, config: Config): - self.config: Config = config - - -class AnalogInput(CarlosPeripheral, ABC): - """Common base class for all analog input peripherals.""" - - @abstractmethod - def read(self) -> dict[str, float]: - """Reads the value of the analog input. The return value is a dictionary - containing the value of the analog input.""" - pass - - async def read_async(self) -> dict[str, float]: - """Reads the value of the analog input asynchronously. The return value is a dictionary - containing the value of the analog input.""" - - loop = asyncio.get_running_loop() - - with concurrent.futures.ThreadPoolExecutor() as pool: - return await loop.run_in_executor(executor=pool, func=self.read) - - -class DigitalOutput(CarlosPeripheral, ABC): - """Common base class for all digital output peripherals.""" - - @abstractmethod - def set(self, value: bool): - pass - - -CarlosIO = AnalogInput | DigitalOutput From 09b5c27208dbc0ab9be5173eaa005bd4b1edb756 Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 18:45:39 +0200 Subject: [PATCH 14/57] fix somes tuff and load modules --- .../carlos/edge/device/__init__.py | 9 +++- .../carlos/edge/device/config.py | 22 +++++----- .../carlos/edge/device/config_test.py | 3 +- .../carlos/edge/device/io/__init__.py | 9 ++++ .../carlos/edge/device/io/device_metrics.py | 6 +-- .../carlos/edge/device/io/dht11.py | 6 +-- .../carlos/edge/device/io/relay.py | 8 ++-- .../carlos/edge/device/io/si1145.py | 6 +-- .../carlos/edge/device/runtime.py | 15 ++++--- lib/py_edge_device/pyproject.toml | 2 + lib/py_edge_device/tests/__init__.py | 0 .../tests/test_data/__init__.py | 3 ++ .../tests/test_data/device_config | 42 +++++++++++++++++++ .../carlos/edge/interface/device/config.py | 2 +- .../carlos/edge/interface/device/io.py | 2 +- 15 files changed, 99 insertions(+), 36 deletions(-) create mode 100644 lib/py_edge_device/tests/__init__.py create mode 100644 lib/py_edge_device/tests/test_data/__init__.py create mode 100644 lib/py_edge_device/tests/test_data/device_config diff --git a/lib/py_edge_device/carlos/edge/device/__init__.py b/lib/py_edge_device/carlos/edge/device/__init__.py index a2f3875c..752865c9 100644 --- a/lib/py_edge_device/carlos/edge/device/__init__.py +++ b/lib/py_edge_device/carlos/edge/device/__init__.py @@ -1,4 +1,9 @@ -__all__ = ["DeviceRuntime", "read_config", "DeviceConfig"] +__all__ = [ + "DeviceRuntime", +] -from .config import DeviceConfig, read_config +from .io import load_supported_io from .runtime import DeviceRuntime + +# Ensures that all supported IO modules are loaded and registered +load_supported_io() diff --git a/lib/py_edge_device/carlos/edge/device/config.py b/lib/py_edge_device/carlos/edge/device/config.py index e99c4b1e..b770b2c2 100644 --- a/lib/py_edge_device/carlos/edge/device/config.py +++ b/lib/py_edge_device/carlos/edge/device/config.py @@ -2,9 +2,8 @@ configuration of the application.""" __all__ = [ - "read_config", + "load_io", "read_config_file", - "write_config", "write_config_file", ] @@ -12,11 +11,13 @@ from typing import TypeVar import yaml -from carlos.edge.interface.device import DeviceConfig +from carlos.edge.interface.device import CarlosIO, IoFactory from pydantic import BaseModel from carlos.edge.device.constants import CONFIG_FILE_NAME +from .io import load_supported_io + Config = TypeVar("Config", bound=BaseModel) @@ -42,15 +43,14 @@ def write_config_file(path: Path, config: Config): ) -def read_config() -> DeviceConfig: # pragma: no cover +def load_io() -> list[CarlosIO]: # pragma: no cover """Reads the configuration from the default location.""" - return read_config_file( - path=Path.cwd() / CONFIG_FILE_NAME, - schema=DeviceConfig, - ) + load_supported_io() + + with open(Path.cwd() / CONFIG_FILE_NAME, "r") as file: + raw_config = yaml.safe_load(file) + io_factory = IoFactory() -def write_config(config: DeviceConfig): # pragma: no cover - """Writes the configuration to the default location.""" - write_config_file(path=Path.cwd() / CONFIG_FILE_NAME, config=config) + return [io_factory.build(config) for config in raw_config.get("io", [])] diff --git a/lib/py_edge_device/carlos/edge/device/config_test.py b/lib/py_edge_device/carlos/edge/device/config_test.py index f0c3d160..7ff98bb3 100644 --- a/lib/py_edge_device/carlos/edge/device/config_test.py +++ b/lib/py_edge_device/carlos/edge/device/config_test.py @@ -1,5 +1,4 @@ from pathlib import Path -from uuid import uuid4 import pytest from carlos.edge.interface.device import DeviceConfig @@ -15,7 +14,7 @@ def test_config_file_io(tmp_path: Path): with pytest.raises(FileNotFoundError): read_config_file(cfg_path, DeviceConfig) - config = DeviceConfig(device_id=uuid4()) + config = DeviceConfig(io=[]) write_config_file(cfg_path, config) diff --git a/lib/py_edge_device/carlos/edge/device/io/__init__.py b/lib/py_edge_device/carlos/edge/device/io/__init__.py index e69de29b..59e56c49 100644 --- a/lib/py_edge_device/carlos/edge/device/io/__init__.py +++ b/lib/py_edge_device/carlos/edge/device/io/__init__.py @@ -0,0 +1,9 @@ +import importlib +from pathlib import Path + + +def load_supported_io(): + """Loads all supported IO modules.""" + for module in Path(__file__).parent.glob("*.py"): + if not module.name.startswith("_"): + importlib.import_module(f"{__package__}.{module.stem}") diff --git a/lib/py_edge_device/carlos/edge/device/io/device_metrics.py b/lib/py_edge_device/carlos/edge/device/io/device_metrics.py index e70c1a30..e02f2c83 100644 --- a/lib/py_edge_device/carlos/edge/device/io/device_metrics.py +++ b/lib/py_edge_device/carlos/edge/device/io/device_metrics.py @@ -1,11 +1,11 @@ import psutil -from carlos.edge.interface.device import AnalogInput, IOConfig, peripheral_registry +from carlos.edge.interface.device import AnalogInput, IoConfig, IoFactory class DeviceMetrics(AnalogInput): """Provides the metrics of the device.""" - def __init__(self, config: IOConfig): + def __init__(self, config: IoConfig): super().__init__(config=config) @@ -29,4 +29,4 @@ def _read_cpu_temp() -> float: return 0.0 -peripheral_registry.register(ptype=__name__, config=IOConfig, factory=DeviceMetrics) +IoFactory().register(ptype=__name__, config=IoConfig, factory=DeviceMetrics) diff --git a/lib/py_edge_device/carlos/edge/device/io/dht11.py b/lib/py_edge_device/carlos/edge/device/io/dht11.py index 037d0809..722f22b2 100644 --- a/lib/py_edge_device/carlos/edge/device/io/dht11.py +++ b/lib/py_edge_device/carlos/edge/device/io/dht11.py @@ -1,4 +1,4 @@ -from carlos.edge.interface.device import AnalogInput, GPIOConfig, peripheral_registry +from carlos.edge.interface.device import AnalogInput, GpioConfig, IoFactory from ._dhtxx import DHT, DHTType @@ -6,7 +6,7 @@ class DHT11(AnalogInput): """DHT11 Temperature and Humidity Sensor.""" - def __init__(self, config: GPIOConfig): + def __init__(self, config: GpioConfig): super().__init__(config=config) @@ -30,4 +30,4 @@ def read(self) -> dict[str, float]: raise RuntimeError("Could not read DHT11 sensor.") -peripheral_registry.register(ptype=__name__, config=GPIOConfig, factory=DHT11) +IoFactory().register(ptype=__name__, config=GpioConfig, factory=DHT11) diff --git a/lib/py_edge_device/carlos/edge/device/io/relay.py b/lib/py_edge_device/carlos/edge/device/io/relay.py index 02f7997d..6a17db3b 100644 --- a/lib/py_edge_device/carlos/edge/device/io/relay.py +++ b/lib/py_edge_device/carlos/edge/device/io/relay.py @@ -1,4 +1,4 @@ -from carlos.edge.interface.device import DigitalOutput, GPIOConfig, peripheral_registry +from carlos.edge.interface.device import DigitalOutput, GpioConfig, IoFactory from carlos.edge.device.protocol import GPIO @@ -6,14 +6,14 @@ class Relay(DigitalOutput): """Relay.""" - def __init__(self, config: GPIOConfig): + def __init__(self, config: GpioConfig): super().__init__(config=config) GPIO.setup(self.config.pin, GPIO.OUT) - def write(self, value: bool): + def set(self, value: bool): """Writes the value to the relay.""" GPIO.output(self.config.pin, value) -peripheral_registry.register(ptype=__name__, config=GPIOConfig, factory=Relay) +IoFactory().register(ptype=__name__, config=GpioConfig, factory=Relay) diff --git a/lib/py_edge_device/carlos/edge/device/io/si1145.py b/lib/py_edge_device/carlos/edge/device/io/si1145.py index 3804c414..300b6421 100644 --- a/lib/py_edge_device/carlos/edge/device/io/si1145.py +++ b/lib/py_edge_device/carlos/edge/device/io/si1145.py @@ -1,13 +1,13 @@ import time -from carlos.edge.interface.device import AnalogInput, I2CConfig, peripheral_registry +from carlos.edge.interface.device import AnalogInput, I2cConfig, IoFactory from carlos.edge.device.protocol import I2C class SI1145(AnalogInput): - def __init__(self, config: I2CConfig): + def __init__(self, config: I2cConfig): if config.address != SDL_Pi_SI1145.ADDR: raise ValueError("The address of the SI1145 sensor must be 0x60.") @@ -34,7 +34,7 @@ def read(self) -> dict[str, float]: } -peripheral_registry.register(ptype=__name__, config=I2CConfig, factory=SI1145) +IoFactory().register(ptype=__name__, config=I2cConfig, factory=SI1145) class SDL_Pi_SI1145: diff --git a/lib/py_edge_device/carlos/edge/device/runtime.py b/lib/py_edge_device/carlos/edge/device/runtime.py index 9fc319a1..3312395b 100644 --- a/lib/py_edge_device/carlos/edge/device/runtime.py +++ b/lib/py_edge_device/carlos/edge/device/runtime.py @@ -6,26 +6,29 @@ from apscheduler import AsyncScheduler from apscheduler.triggers.interval import IntervalTrigger -from carlos.edge.interface import EdgeConnectionDisconnected, EdgeProtocol -from carlos.edge.interface.device import DeviceConfig +from carlos.edge.interface import DeviceId, EdgeConnectionDisconnected, EdgeProtocol from carlos.edge.interface.protocol import PING from loguru import logger from .communication import DeviceCommunicationHandler +from .config import load_io # We don't cover this in the unit tests. This needs to be tested in an integration test. class DeviceRuntime: # pragma: no cover - def __init__(self, config: DeviceConfig, protocol: EdgeProtocol): + def __init__(self, device_id: DeviceId, protocol: EdgeProtocol): """Initializes the device runtime. - :param config: The configuration of the device. + :param device_id: The unique identifier of the device. + :param protocol: The concrete implementation of the EdgeProtocol. """ - self.config = config + self.device_id = device_id self.protocol = protocol + self.ios = load_io() + async def run(self): """Runs the device runtime.""" @@ -37,7 +40,7 @@ async def run(self): ) communication_handler = DeviceCommunicationHandler( - protocol=self.protocol, device_id=self.config.device_id + protocol=self.protocol, device_id=self.device_id ) if not self.protocol.is_connected: diff --git a/lib/py_edge_device/pyproject.toml b/lib/py_edge_device/pyproject.toml index 85cacf09..a9d893d8 100644 --- a/lib/py_edge_device/pyproject.toml +++ b/lib/py_edge_device/pyproject.toml @@ -66,4 +66,6 @@ exclude_lines = [ omit = [ # omit all tests "*_test.py", + # depends on actual device and hardware + "carlos/edge/device/io/*.py" ] diff --git a/lib/py_edge_device/tests/__init__.py b/lib/py_edge_device/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lib/py_edge_device/tests/test_data/__init__.py b/lib/py_edge_device/tests/test_data/__init__.py new file mode 100644 index 00000000..e62dbf07 --- /dev/null +++ b/lib/py_edge_device/tests/test_data/__init__.py @@ -0,0 +1,3 @@ +from pathlib import Path + +TEST_DATA_DIR = Path(__file__).parent diff --git a/lib/py_edge_device/tests/test_data/device_config b/lib/py_edge_device/tests/test_data/device_config new file mode 100644 index 00000000..1986eedb --- /dev/null +++ b/lib/py_edge_device/tests/test_data/device_config @@ -0,0 +1,42 @@ +io: + - identifier: temp-humi-intern + protocol: gpio + type: dht11 + direction: in + pin: 5 + - identifier: uv-light + protocol: i2c + type: si1145 + address: 0x60 + - identifier: pump + protocol: gpio + type: relay + pin: 20 + - identifier: valve-1 + protocol: gpio + type: relay + pin: 21 + - identifier: valve-2 + protocol: gpio + type: relay + pin: 22 + - identifier: valve-3 + protocol: gpio + type: relay + pin: 23 + - identifier: valve-4 + protocol: gpio + type: relay + pin: 24 + - identifier: spare-1 + protocol: gpio + type: relay + pin: 25 + - identifier: spare-2 + protocol: gpio + type: relay + pin: 26 + - identifier: spare-3 + protocol: gpio + type: relay + pin: 27 diff --git a/lib/py_edge_interface/carlos/edge/interface/device/config.py b/lib/py_edge_interface/carlos/edge/interface/device/config.py index 9bec0b19..99fdc97c 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/config.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/config.py @@ -80,7 +80,7 @@ class DigitalGpioOutputConfig(GpioConfig): ) -class I2cConfig(BaseModel): +class I2cConfig(IoConfig): """Defines a single input configuration.""" protocol: Literal["i2c"] = Field( diff --git a/lib/py_edge_interface/carlos/edge/interface/device/io.py b/lib/py_edge_interface/carlos/edge/interface/device/io.py index eb6db0a8..8a86e4d0 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/io.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/io.py @@ -128,7 +128,7 @@ def validate_device_address_space(configs: Iterable[IoConfig]): f"Please ensure that each GPIO pin is configured only once." ) - i2c_configs: list[I2cConfig] = [io for io in configs if isinstance(io, I2cConfig)] # type: ignore[misc] # noqa: E501 + i2c_configs: list[I2cConfig] = [io for io in configs if isinstance(io, I2cConfig)] if i2c_configs: if any(gpio.pin in I2C_PINS for gpio in gpio_configs): raise ValueError( From df197adb4e12ea72a8d455194bada0c2b2f73a72 Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 21:42:10 +0200 Subject: [PATCH 15/57] ignore some stuff --- .../carlos/edge/device/config_test.py | 13 +++-- .../carlos/edge/device/protocol/_gpio_mock.py | 58 +++++++++++++++++++ .../carlos/edge/device/protocol/gpio.py | 9 ++- .../carlos/edge/device/protocol/i2c.py | 4 +- lib/py_edge_device/poetry.lock | 12 +--- lib/py_edge_device/pyproject.toml | 1 - 6 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 lib/py_edge_device/carlos/edge/device/protocol/_gpio_mock.py diff --git a/lib/py_edge_device/carlos/edge/device/config_test.py b/lib/py_edge_device/carlos/edge/device/config_test.py index 7ff98bb3..5edfe36f 100644 --- a/lib/py_edge_device/carlos/edge/device/config_test.py +++ b/lib/py_edge_device/carlos/edge/device/config_test.py @@ -1,7 +1,7 @@ from pathlib import Path import pytest -from carlos.edge.interface.device import DeviceConfig +from carlos.edge.interface.device import GpioConfig from carlos.edge.device.config import read_config_file, write_config_file @@ -12,10 +12,15 @@ def test_config_file_io(tmp_path: Path): cfg_path = tmp_path / "config" with pytest.raises(FileNotFoundError): - read_config_file(cfg_path, DeviceConfig) + read_config_file(cfg_path, GpioConfig) - config = DeviceConfig(io=[]) + config = GpioConfig( + identifier="test-config-file-io", + ptype="test-config-file-io", + direction="input", + pin=7, + ) write_config_file(cfg_path, config) - assert read_config_file(cfg_path, DeviceConfig) == config + assert read_config_file(cfg_path, GpioConfig) == config diff --git a/lib/py_edge_device/carlos/edge/device/protocol/_gpio_mock.py b/lib/py_edge_device/carlos/edge/device/protocol/_gpio_mock.py new file mode 100644 index 00000000..0ae44470 --- /dev/null +++ b/lib/py_edge_device/carlos/edge/device/protocol/_gpio_mock.py @@ -0,0 +1,58 @@ +__all__ = ["GPIO"] + +from loguru import logger + + +class GpioMock: # pragma: no cover + + LOW = 0 + HIGH = 1 + + IN = 0 + OUT = 1 + + PUD_OFF = 0 + PUD_DOWN = 1 + PUD_UP = 2 + + BOARD = 10 + BCM = 11 + + def __init__(self): + self._pins: list[int] = [] + + def setwarnings(self, state: bool): + pass + + def setmode(self, mode: int): + pass + + def setup(self, pin: int | list[int], mode: int): + if isinstance(pin, list): + for p in pin: + self.setup(p, mode) + return + + if pin not in self._pins: + self._pins.append(pin) + logger.debug(f"Setup pin {pin} in mode {mode}") + + def output(self, pin: int, state: bool): + if pin in self._pins: + logger.debug(f"Set pin {pin} to {'HIGH' if state else 'LOW'}") + else: + raise ValueError(f"Pin {pin} not set up") + + def input(self, pin: int) -> bool: + if pin in self._pins: + logger.debug(f"Reading input from pin {pin}") + return False # Dummy value, always returning False for simplicity + else: + raise ValueError(f"Pin {pin} not set up") + + def cleanup(self): + self._pins.clear() + logger.debug("Cleaned up GPIO pins") + + +GPIO = GpioMock() diff --git a/lib/py_edge_device/carlos/edge/device/protocol/gpio.py b/lib/py_edge_device/carlos/edge/device/protocol/gpio.py index 1e341a2c..9c4f85a2 100644 --- a/lib/py_edge_device/carlos/edge/device/protocol/gpio.py +++ b/lib/py_edge_device/carlos/edge/device/protocol/gpio.py @@ -1,9 +1,16 @@ __all__ = ["GPIO"] +import traceback +import warnings + try: from RPi import GPIO # type: ignore except ImportError: - from RPiSim.GPIO import GPIO # type: ignore + warnings.warn( + "RPi.GPIO not available. Fallback tom mocked GPIO instead. " + f"{traceback.format_exc()}" + ) + from ._gpio_mock import GPIO # type: ignore # Choose the GPIO mode globally GPIO.setmode(GPIO.BCM) diff --git a/lib/py_edge_device/carlos/edge/device/protocol/i2c.py b/lib/py_edge_device/carlos/edge/device/protocol/i2c.py index 3688284a..bfa24dd4 100644 --- a/lib/py_edge_device/carlos/edge/device/protocol/i2c.py +++ b/lib/py_edge_device/carlos/edge/device/protocol/i2c.py @@ -9,12 +9,12 @@ I2C_LOCK = RLock() -class I2cLock: +class I2cLock: # pragma: no cover def __new__(cls): return I2C_LOCK -class I2C: +class I2C: # pragma: no cover """This class is based on, but heavily modified from, the Adafruit_I2C class""" def __init__(self, address: int, bus: int | None = None): diff --git a/lib/py_edge_device/poetry.lock b/lib/py_edge_device/poetry.lock index be302a1a..7954ef1e 100644 --- a/lib/py_edge_device/poetry.lock +++ b/lib/py_edge_device/poetry.lock @@ -403,16 +403,6 @@ websocket-client = ">=0.32.0" [package.extras] ssh = ["paramiko (>=2.4.3)"] -[[package]] -name = "gpiosimulator" -version = "0.1" -description = "Raspberry Pi GPIO simulator" -optional = false -python-versions = "*" -files = [ - {file = "GPIOSimulator-0.1.tar.gz", hash = "sha256:08a221d03c9c5bd137d573b24aa0ebb9871760b12b8a1090392cbded3d06fee8"}, -] - [[package]] name = "greenlet" version = "3.0.3" @@ -1270,4 +1260,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.12" -content-hash = "ec32e3ddd34a892e48827ce6d04bcde34f28a77371d3e6a53ed6ae37a03ccafc" +content-hash = "1360c9bb3a5033c9673e55b9ddf9db48210ddf6dda4e73057645c9599f01f322" diff --git a/lib/py_edge_device/pyproject.toml b/lib/py_edge_device/pyproject.toml index a9d893d8..25a77e36 100644 --- a/lib/py_edge_device/pyproject.toml +++ b/lib/py_edge_device/pyproject.toml @@ -17,7 +17,6 @@ pydantic = "^2.6.4" pyyaml = "^6.0.1" "carlos.edge.interface" = {path = "../py_edge_interface"} psutil = "^5.9.8" -gpiosimulator = "^0.1" rpi-gpio = {version = "^0.7.1", markers = "platform_machine == 'armv7l' or platform_machine == 'aarch64'"} smbus2 = "^0.4.3" From e7e08a3a04f27672e862320dbbaf089be5ead1c0 Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 22:47:50 +0200 Subject: [PATCH 16/57] manage dynamic loading --- .../carlos/edge/device/__init__.py | 4 -- .../carlos/edge/device/config.py | 16 +++--- .../carlos/edge/device/config_test.py | 19 ++++++- .../carlos/edge/device/io/__init__.py | 9 --- .../carlos/edge/device/io/device_metrics.py | 3 + .../carlos/edge/device/io/dht11.py | 9 ++- .../carlos/edge/device/io/relay.py | 1 + .../carlos/edge/device/io/si1145.py | 12 +++- .../carlos/edge/device/runtime.py | 8 +++ .../tests/test_data/__init__.py | 4 +- .../tests/test_data/device_config | 31 ++++++---- .../carlos/edge/interface/device/config.py | 57 ++++++++++++------- .../carlos/edge/interface/device/io.py | 49 ++++++++++++---- 13 files changed, 151 insertions(+), 71 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/__init__.py b/lib/py_edge_device/carlos/edge/device/__init__.py index 752865c9..2d2b455c 100644 --- a/lib/py_edge_device/carlos/edge/device/__init__.py +++ b/lib/py_edge_device/carlos/edge/device/__init__.py @@ -2,8 +2,4 @@ "DeviceRuntime", ] -from .io import load_supported_io from .runtime import DeviceRuntime - -# Ensures that all supported IO modules are loaded and registered -load_supported_io() diff --git a/lib/py_edge_device/carlos/edge/device/config.py b/lib/py_edge_device/carlos/edge/device/config.py index b770b2c2..d87c718c 100644 --- a/lib/py_edge_device/carlos/edge/device/config.py +++ b/lib/py_edge_device/carlos/edge/device/config.py @@ -12,12 +12,11 @@ import yaml from carlos.edge.interface.device import CarlosIO, IoFactory +from loguru import logger from pydantic import BaseModel from carlos.edge.device.constants import CONFIG_FILE_NAME -from .io import load_supported_io - Config = TypeVar("Config", bound=BaseModel) @@ -43,14 +42,17 @@ def write_config_file(path: Path, config: Config): ) -def load_io() -> list[CarlosIO]: # pragma: no cover +def load_io(config_dir: Path | None = None) -> list[CarlosIO]: """Reads the configuration from the default location.""" + config_dir = config_dir or Path.cwd() - load_supported_io() - - with open(Path.cwd() / CONFIG_FILE_NAME, "r") as file: + with open(config_dir / CONFIG_FILE_NAME, "r") as file: raw_config = yaml.safe_load(file) io_factory = IoFactory() - return [io_factory.build(config) for config in raw_config.get("io", [])] + ios = [io_factory.build(config) for config in raw_config.get("io", [])] + + logger.info(f"Loaded {len(ios)} IOs: {', '.join(str(io) for io in ios)}") + + return ios diff --git a/lib/py_edge_device/carlos/edge/device/config_test.py b/lib/py_edge_device/carlos/edge/device/config_test.py index 5edfe36f..a518e07c 100644 --- a/lib/py_edge_device/carlos/edge/device/config_test.py +++ b/lib/py_edge_device/carlos/edge/device/config_test.py @@ -1,9 +1,10 @@ from pathlib import Path import pytest -from carlos.edge.interface.device import GpioConfig +from carlos.edge.interface.device import AnalogInput, DigitalOutput, GpioConfig -from carlos.edge.device.config import read_config_file, write_config_file +from carlos.edge.device.config import load_io, read_config_file, write_config_file +from tests.test_data import EXPECTED_IO_COUNT, TEST_DEVICE_WORKDIR def test_config_file_io(tmp_path: Path): @@ -16,7 +17,7 @@ def test_config_file_io(tmp_path: Path): config = GpioConfig( identifier="test-config-file-io", - ptype="test-config-file-io", + module="carlos.edge.device.io.dht11", direction="input", pin=7, ) @@ -24,3 +25,15 @@ def test_config_file_io(tmp_path: Path): write_config_file(cfg_path, config) assert read_config_file(cfg_path, GpioConfig) == config + + +def test_load_io(): + """This test ensures that the IOs are loaded correctly.""" + + io = load_io(config_dir=TEST_DEVICE_WORKDIR) + + assert len(io) == EXPECTED_IO_COUNT, "The number of IOs does not match." + + assert all( + isinstance(io, (AnalogInput, DigitalOutput)) for io in io + ), "Not all IOs are of the correct type." diff --git a/lib/py_edge_device/carlos/edge/device/io/__init__.py b/lib/py_edge_device/carlos/edge/device/io/__init__.py index 59e56c49..e69de29b 100644 --- a/lib/py_edge_device/carlos/edge/device/io/__init__.py +++ b/lib/py_edge_device/carlos/edge/device/io/__init__.py @@ -1,9 +0,0 @@ -import importlib -from pathlib import Path - - -def load_supported_io(): - """Loads all supported IO modules.""" - for module in Path(__file__).parent.glob("*.py"): - if not module.name.startswith("_"): - importlib.import_module(f"{__package__}.{module.stem}") diff --git a/lib/py_edge_device/carlos/edge/device/io/device_metrics.py b/lib/py_edge_device/carlos/edge/device/io/device_metrics.py index e02f2c83..ed6ed79f 100644 --- a/lib/py_edge_device/carlos/edge/device/io/device_metrics.py +++ b/lib/py_edge_device/carlos/edge/device/io/device_metrics.py @@ -9,6 +9,9 @@ def __init__(self, config: IoConfig): super().__init__(config=config) + def setup(self): + pass + def read(self) -> dict[str, float]: """Reads the device metrics.""" diff --git a/lib/py_edge_device/carlos/edge/device/io/dht11.py b/lib/py_edge_device/carlos/edge/device/io/dht11.py index 722f22b2..7460d610 100644 --- a/lib/py_edge_device/carlos/edge/device/io/dht11.py +++ b/lib/py_edge_device/carlos/edge/device/io/dht11.py @@ -10,11 +10,18 @@ def __init__(self, config: GpioConfig): super().__init__(config=config) - self._dht = DHT(dht_type=DHTType.DHT11, pin=config.pin) + self._dht: DHT | None = None + + def setup(self): + """Sets up the DHT11 sensor.""" + + self._dht = DHT(dht_type=DHTType.DHT11, pin=self.config.pin) def read(self) -> dict[str, float]: """Reads the temperature and humidity.""" + assert self._dht is not None, "The DHT sensor has not been initialized." + # Reading the DHT sensor is quite unreliable, as the device is not a real-time # device. Thus, we just try it a couple of times and fail if it does not work. for i in range(16): diff --git a/lib/py_edge_device/carlos/edge/device/io/relay.py b/lib/py_edge_device/carlos/edge/device/io/relay.py index 6a17db3b..be0be8a7 100644 --- a/lib/py_edge_device/carlos/edge/device/io/relay.py +++ b/lib/py_edge_device/carlos/edge/device/io/relay.py @@ -9,6 +9,7 @@ class Relay(DigitalOutput): def __init__(self, config: GpioConfig): super().__init__(config=config) + def setup(self): GPIO.setup(self.config.pin, GPIO.OUT) def set(self, value: bool): diff --git a/lib/py_edge_device/carlos/edge/device/io/si1145.py b/lib/py_edge_device/carlos/edge/device/io/si1145.py index 300b6421..8bf3f18a 100644 --- a/lib/py_edge_device/carlos/edge/device/io/si1145.py +++ b/lib/py_edge_device/carlos/edge/device/io/si1145.py @@ -9,16 +9,24 @@ class SI1145(AnalogInput): def __init__(self, config: I2cConfig): - if config.address != SDL_Pi_SI1145.ADDR: - raise ValueError("The address of the SI1145 sensor must be 0x60.") + if config.address_int != SDL_Pi_SI1145.ADDR: + raise ValueError( + f"The address of the SI1145 sensor must be 0x60. Got {config.address} instead" + ) super().__init__(config=config) + self._si1145: SDL_Pi_SI1145 | None = None + + def setup(self): + self._si1145 = SDL_Pi_SI1145() def read(self) -> dict[str, float]: """Reads various light levels from the sensor.""" + assert self._si1145 is not None, "The sensor has not been set up." + vis_raw = self._si1145.read_visible() vis_lux = self._si1145.convert_visible_to_lux(vis_raw) ir_raw = self._si1145.read_ir() diff --git a/lib/py_edge_device/carlos/edge/device/runtime.py b/lib/py_edge_device/carlos/edge/device/runtime.py index 3312395b..f6a76dd3 100644 --- a/lib/py_edge_device/carlos/edge/device/runtime.py +++ b/lib/py_edge_device/carlos/edge/device/runtime.py @@ -63,6 +63,14 @@ async def run(self): except EdgeConnectionDisconnected: await self.protocol.connect() + def _setup_io(self): + """Sets up the I/O peripherals.""" + for io in self.ios: + logger.debug( + f"Setting up I/O peripheral {io.config.identifier} ({io.config.module})." + ) + io.setup() + async def send_ping( communication_handler: DeviceCommunicationHandler, diff --git a/lib/py_edge_device/tests/test_data/__init__.py b/lib/py_edge_device/tests/test_data/__init__.py index e62dbf07..146715dd 100644 --- a/lib/py_edge_device/tests/test_data/__init__.py +++ b/lib/py_edge_device/tests/test_data/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path -TEST_DATA_DIR = Path(__file__).parent +TEST_DEVICE_WORKDIR = Path(__file__).parent + +EXPECTED_IO_COUNT = 10 diff --git a/lib/py_edge_device/tests/test_data/device_config b/lib/py_edge_device/tests/test_data/device_config index 1986eedb..c5eb5417 100644 --- a/lib/py_edge_device/tests/test_data/device_config +++ b/lib/py_edge_device/tests/test_data/device_config @@ -1,42 +1,51 @@ io: - identifier: temp-humi-intern + module: dht11 + direction: input protocol: gpio - type: dht11 - direction: in pin: 5 - identifier: uv-light + module: si1145 + direction: input protocol: i2c - type: si1145 address: 0x60 - identifier: pump + module: relay + direction: output protocol: gpio - type: relay pin: 20 - identifier: valve-1 + module: relay + direction: output protocol: gpio - type: relay pin: 21 - identifier: valve-2 + module: relay + direction: output protocol: gpio - type: relay pin: 22 - identifier: valve-3 + module: relay + direction: output protocol: gpio - type: relay pin: 23 - identifier: valve-4 + module: carlos.edge.device.io.relay + direction: output protocol: gpio - type: relay pin: 24 - identifier: spare-1 + module: carlos.edge.device.io.relay + direction: output protocol: gpio - type: relay pin: 25 - identifier: spare-2 + module: carlos.edge.device.io.relay + direction: output protocol: gpio - type: relay pin: 26 - identifier: spare-3 + module: carlos.edge.device.io.relay + direction: output protocol: gpio - type: relay pin: 27 diff --git a/lib/py_edge_interface/carlos/edge/interface/device/config.py b/lib/py_edge_interface/carlos/edge/interface/device/config.py index 99fdc97c..550e5e7a 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/config.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/config.py @@ -1,7 +1,7 @@ -__all__ = ["GpioConfig", "I2cConfig", "IoConfig", "IoPtypeDict"] +__all__ = ["GpioConfig", "I2cConfig", "IoConfig"] -from abc import ABC -from typing import Literal, TypedDict +import importlib +from typing import Literal from pydantic import BaseModel, Field, field_validator @@ -10,11 +10,7 @@ # 3.3V, 2, 3, 4, GND, 17, 27, 22, 3.3V, 10, 9, 11, GND, ID EEPROM, 5, 6, 13, 19, 26, GND # Inner pins # noqa -class IoPtypeDict(TypedDict): - ptype: str - - -class IoConfig(BaseModel, ABC): +class IoConfig(BaseModel): """Common base class for all IO configurations.""" identifier: str = Field( @@ -23,18 +19,40 @@ class IoConfig(BaseModel, ABC): "It is used to allow changing addresses, pins if required later.", ) - ptype: str = Field( + module: str = Field( ..., - description="A string that uniquely identifies the type of IO. Usually the " - "name of the sensor or actuator in lower case letters.", + description="The module name that will be imported and registers itself in " + "the IoFactory. If the module does not contain `.` it is assumed " + "to be a built-in module.", ) + @field_validator("module", mode="after") + def _validate_module(cls, value): + """Converts a module name to a full module path.""" + + # check if the given module exists in the current working directory. + try: + importlib.import_module(value) + except ModuleNotFoundError: + abs_module = "carlos.edge.device.io" + "." + value + try: + importlib.import_module(abs_module) + except ModuleNotFoundError: # pragma: no cover + raise ValueError( + f"The module {value} ({abs_module}) does not exist." + ) + value = abs_module + + return value + + +class DirectionMixin(BaseModel): direction: Literal["input", "output"] = Field( ..., description="The direction of the IO." ) -class GpioConfig(IoConfig): +class GpioConfig(IoConfig, DirectionMixin): """Defines a single input configuration.""" protocol: Literal["gpio"] = Field( @@ -72,15 +90,7 @@ class GpioConfig(IoConfig): ] = Field(..., description="The GPIO pin number.") -class DigitalGpioOutputConfig(GpioConfig): - """Defines a single digital output configuration.""" - - direction: Literal["output"] = Field( - ..., description="The direction of the GPIO pin." - ) - - -class I2cConfig(IoConfig): +class I2cConfig(IoConfig, DirectionMixin): """Defines a single input configuration.""" protocol: Literal["i2c"] = Field( @@ -109,3 +119,8 @@ def validate_address(cls, value): raise ValueError("The valid I2C address range is 0x03 to 0x77.") return hex(value) + + @property + def address_int(self): + """Returns the I2C address as an integer.""" + return int(self.address, 16) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/io.py b/lib/py_edge_interface/carlos/edge/interface/device/io.py index 8a86e4d0..ef39627e 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/io.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/io.py @@ -9,9 +9,9 @@ import concurrent.futures from abc import ABC, abstractmethod from collections import namedtuple -from typing import Callable, Generic, Iterable, TypeVar +from typing import Any, Callable, Generic, Iterable, TypeVar -from .config import GpioConfig, I2cConfig, IoConfig, IoPtypeDict +from .config import GpioConfig, I2cConfig, IoConfig IoConfigTypeVar = TypeVar("IoConfigTypeVar", bound=IoConfig) @@ -22,6 +22,15 @@ class CarlosPeripheral(ABC, Generic[IoConfigTypeVar]): def __init__(self, config: IoConfigTypeVar): self.config: IoConfigTypeVar = config + def __str__(self): + return f"{self.config.identifier} ({self.config.module})" + + @abstractmethod + def setup(self): + """Sets up the peripheral. This is required for testing. As the test runner + is not able to run the setup method of the peripheral outside the device.""" + pass + class AnalogInput(CarlosPeripheral, ABC): """Common base class for all analog input peripherals.""" @@ -59,12 +68,12 @@ class IoFactory: """A singleton factory for io peripherals.""" _instance = None - _ptype_to_io: dict[str, FactoryItem] = {} + _module_to_io: dict[str, FactoryItem] = {} def __new__(cls): if cls._instance is None: cls._instance = super(IoFactory, cls).__new__(cls) - cls._instance._ptype_to_io = {} + cls._instance._module_to_io = {} return cls._instance @@ -81,20 +90,36 @@ def register( :param factory: The peripheral factory function. """ - if ptype in self._ptype_to_io: + if not issubclass(config, IoConfig): + raise ValueError( + "The config must be a subclass of IoConfig. " + "Please ensure that the config class is a subclass of IoConfig." + ) + + if ptype in self._module_to_io: raise RuntimeError(f"The peripheral {ptype} is already registered.") - self._ptype_to_io[ptype] = FactoryItem(config, factory) + self._module_to_io[ptype] = FactoryItem(config, factory) - def build(self, config: IoPtypeDict) -> CarlosIO: - """Builds a IO object from its configuration.""" + def build(self, config: dict[str, Any]) -> CarlosIO: + """Builds a IO object from its configuration. - ptype = config["ptype"] + :param config: The raw configuration. The schema must adhere to the + IoConfig model. But we require the full config as the ios may require + additional parameters. + :returns: The IO object. + """ - if type not in self._ptype_to_io: - raise ValueError(f"The peripheral {ptype} is not registered.") + io_config = IoConfig.model_validate(config) + + if io_config.module not in self._module_to_io: + raise RuntimeError( + f"The peripheral {io_config.module} is not registered." + f"Make sure to register `IoFactory().register(...)` " + f"the peripheral before building it." + ) - entry = self._ptype_to_io[ptype] + entry = self._module_to_io[io_config.module] return entry.factory(entry.config.model_validate(config)) From 22493ae49cd6378806016c518085a02428744481 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Apr 2024 20:48:34 +0000 Subject: [PATCH 17/57] update lib/py_edge_interface Co-authored-by: flxdot --- lib/py_edge_interface/carlos/edge/interface/device/config.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/config.py b/lib/py_edge_interface/carlos/edge/interface/device/config.py index 550e5e7a..abc2813a 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/config.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/config.py @@ -38,9 +38,7 @@ def _validate_module(cls, value): try: importlib.import_module(abs_module) except ModuleNotFoundError: # pragma: no cover - raise ValueError( - f"The module {value} ({abs_module}) does not exist." - ) + raise ValueError(f"The module {value} ({abs_module}) does not exist.") value = abs_module return value From 8a3724532fe6d3e6e45ba640425cdf2f2bad79c1 Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 22:53:40 +0200 Subject: [PATCH 18/57] proper naming --- .../carlos/edge/device/config_test.py | 2 +- .../carlos/edge/device/runtime.py | 21 +++++++++++++------ .../tests/test_data/device_config | 20 +++++++++--------- .../carlos/edge/interface/device/config.py | 18 ++++++++-------- .../carlos/edge/interface/device/io.py | 21 +++++++++++-------- 5 files changed, 47 insertions(+), 35 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/config_test.py b/lib/py_edge_device/carlos/edge/device/config_test.py index a518e07c..fac73446 100644 --- a/lib/py_edge_device/carlos/edge/device/config_test.py +++ b/lib/py_edge_device/carlos/edge/device/config_test.py @@ -17,7 +17,7 @@ def test_config_file_io(tmp_path: Path): config = GpioConfig( identifier="test-config-file-io", - module="carlos.edge.device.io.dht11", + driver="carlos.edge.device.io.dht11", direction="input", pin=7, ) diff --git a/lib/py_edge_device/carlos/edge/device/runtime.py b/lib/py_edge_device/carlos/edge/device/runtime.py index f6a76dd3..d268cbf8 100644 --- a/lib/py_edge_device/carlos/edge/device/runtime.py +++ b/lib/py_edge_device/carlos/edge/device/runtime.py @@ -7,6 +7,7 @@ from apscheduler import AsyncScheduler from apscheduler.triggers.interval import IntervalTrigger from carlos.edge.interface import DeviceId, EdgeConnectionDisconnected, EdgeProtocol +from carlos.edge.interface.device.io import validate_device_address_space from carlos.edge.interface.protocol import PING from loguru import logger @@ -32,12 +33,7 @@ def __init__(self, device_id: DeviceId, protocol: EdgeProtocol): async def run(self): """Runs the device runtime.""" - logger.add( - sink=Path.cwd() / ".carlos_data" / "device" / "device_log_{time}.log", - level="INFO", - rotation="50 MB", - retention=timedelta(days=60), - ) + self._prepare_runtime() communication_handler = DeviceCommunicationHandler( protocol=self.protocol, device_id=self.device_id @@ -63,6 +59,19 @@ async def run(self): except EdgeConnectionDisconnected: await self.protocol.connect() + def _prepare_runtime(self): + + logger.add( + sink=Path.cwd() / ".carlos_data" / "device" / "device_log_{time}.log", + level="INFO", + rotation="50 MB", + retention=timedelta(days=60), + ) + + validate_device_address_space(self.ios) + + self._setup_io() + def _setup_io(self): """Sets up the I/O peripherals.""" for io in self.ios: diff --git a/lib/py_edge_device/tests/test_data/device_config b/lib/py_edge_device/tests/test_data/device_config index c5eb5417..88a1b7f8 100644 --- a/lib/py_edge_device/tests/test_data/device_config +++ b/lib/py_edge_device/tests/test_data/device_config @@ -1,51 +1,51 @@ io: - identifier: temp-humi-intern - module: dht11 + driver: dht11 direction: input protocol: gpio pin: 5 - identifier: uv-light - module: si1145 + driver: si1145 direction: input protocol: i2c address: 0x60 - identifier: pump - module: relay + driver: relay direction: output protocol: gpio pin: 20 - identifier: valve-1 - module: relay + driver: relay direction: output protocol: gpio pin: 21 - identifier: valve-2 - module: relay + driver: relay direction: output protocol: gpio pin: 22 - identifier: valve-3 - module: relay + driver: relay direction: output protocol: gpio pin: 23 - identifier: valve-4 - module: carlos.edge.device.io.relay + driver: carlos.edge.device.io.relay direction: output protocol: gpio pin: 24 - identifier: spare-1 - module: carlos.edge.device.io.relay + driver: carlos.edge.device.io.relay direction: output protocol: gpio pin: 25 - identifier: spare-2 - module: carlos.edge.device.io.relay + driver: carlos.edge.device.io.relay direction: output protocol: gpio pin: 26 - identifier: spare-3 - module: carlos.edge.device.io.relay + driver: carlos.edge.device.io.relay direction: output protocol: gpio pin: 27 diff --git a/lib/py_edge_interface/carlos/edge/interface/device/config.py b/lib/py_edge_interface/carlos/edge/interface/device/config.py index 550e5e7a..880dc3e1 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/config.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/config.py @@ -19,15 +19,17 @@ class IoConfig(BaseModel): "It is used to allow changing addresses, pins if required later.", ) - module: str = Field( + driver: str = Field( ..., - description="The module name that will be imported and registers itself in " - "the IoFactory. If the module does not contain `.` it is assumed " - "to be a built-in module.", + description="Refers to the module name that implements the IO driver. " + "Built-in drivers located in carlos.edge.device.io module " + "don't need to specify the full path. Each driver module" + "must make a call to the IoFactory.register method to register" + "itself.", ) - @field_validator("module", mode="after") - def _validate_module(cls, value): + @field_validator("driver", mode="after") + def _validate_driver(cls, value): """Converts a module name to a full module path.""" # check if the given module exists in the current working directory. @@ -38,9 +40,7 @@ def _validate_module(cls, value): try: importlib.import_module(abs_module) except ModuleNotFoundError: # pragma: no cover - raise ValueError( - f"The module {value} ({abs_module}) does not exist." - ) + raise ValueError(f"The module {value} ({abs_module}) does not exist.") value = abs_module return value diff --git a/lib/py_edge_interface/carlos/edge/interface/device/io.py b/lib/py_edge_interface/carlos/edge/interface/device/io.py index ef39627e..3d70319d 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/io.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/io.py @@ -68,12 +68,12 @@ class IoFactory: """A singleton factory for io peripherals.""" _instance = None - _module_to_io: dict[str, FactoryItem] = {} + _driver_to_io_type: dict[str, FactoryItem] = {} def __new__(cls): if cls._instance is None: cls._instance = super(IoFactory, cls).__new__(cls) - cls._instance._module_to_io = {} + cls._instance._driver_to_io_type = {} return cls._instance @@ -96,10 +96,10 @@ def register( "Please ensure that the config class is a subclass of IoConfig." ) - if ptype in self._module_to_io: + if ptype in self._driver_to_io_type: raise RuntimeError(f"The peripheral {ptype} is already registered.") - self._module_to_io[ptype] = FactoryItem(config, factory) + self._driver_to_io_type[ptype] = FactoryItem(config, factory) def build(self, config: dict[str, Any]) -> CarlosIO: """Builds a IO object from its configuration. @@ -112,14 +112,14 @@ def build(self, config: dict[str, Any]) -> CarlosIO: io_config = IoConfig.model_validate(config) - if io_config.module not in self._module_to_io: + if io_config.driver not in self._driver_to_io_type: raise RuntimeError( - f"The peripheral {io_config.module} is not registered." + f"The driver {io_config.driver} is not registered." f"Make sure to register `IoFactory().register(...)` " f"the peripheral before building it." ) - entry = self._module_to_io[io_config.module] + entry = self._driver_to_io_type[io_config.driver] return entry.factory(entry.config.model_validate(config)) @@ -128,14 +128,17 @@ def build(self, config: dict[str, Any]) -> CarlosIO: """The Pin numbers designated for I2C communication.""" -def validate_device_address_space(configs: Iterable[IoConfig]): +def validate_device_address_space(ios: Iterable[CarlosIO]): """This function ensures that the configured pins and addresses are unique. - :param configs: The list of IO configurations. + :param ios: The list of IOs to validate. :raises ValueError: If any of the pins or addresses are configured more than once. If the GPIO pins 2 and 3 are when I2C communication is configured. If any of the identifiers are configured more than once. """ + + configs = [io.config for io in ios] + gpio_configs: list[GpioConfig] = [ io for io in configs if isinstance(io, GpioConfig) ] From 9e81c699bdd684a8f431c420df788dbe5b98156e Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 23:15:25 +0200 Subject: [PATCH 19/57] add the ability to test the device --- .../carlos/edge/device/config.py | 14 ++++++- .../carlos/edge/device/io/_dhtxx.py | 38 +++++++++++++++++++ .../carlos/edge/device/io/dht11.py | 32 ++-------------- .../carlos/edge/device/io/dht22.py | 16 ++++++++ .../carlos/edge/device/io/relay.py | 12 +++++- .../carlos/edge/device/io/si1145.py | 13 ++++++- .../carlos/edge/device/runtime.py | 20 +++++++--- .../carlos/edge/interface/device/io.py | 31 ++++++++++++++- 8 files changed, 136 insertions(+), 40 deletions(-) create mode 100644 lib/py_edge_device/carlos/edge/device/io/dht22.py diff --git a/lib/py_edge_device/carlos/edge/device/config.py b/lib/py_edge_device/carlos/edge/device/config.py index d87c718c..7fa84407 100644 --- a/lib/py_edge_device/carlos/edge/device/config.py +++ b/lib/py_edge_device/carlos/edge/device/config.py @@ -11,11 +11,12 @@ from typing import TypeVar import yaml -from carlos.edge.interface.device import CarlosIO, IoFactory +from carlos.edge.interface.device import CarlosIO, IoConfig, IoFactory from loguru import logger from pydantic import BaseModel from carlos.edge.device.constants import CONFIG_FILE_NAME +from carlos.edge.device.io.device_metrics import DeviceMetrics Config = TypeVar("Config", bound=BaseModel) @@ -53,6 +54,17 @@ def load_io(config_dir: Path | None = None) -> list[CarlosIO]: ios = [io_factory.build(config) for config in raw_config.get("io", [])] + # We always want to have some device metrics + if not any(isinstance(io, DeviceMetrics) for io in ios): + ios.insert( + 0, + io_factory.build( + IoConfig( + identifier="__device_metrics__", driver=DeviceMetrics.__module__ + ).model_dump() + ), + ) + logger.info(f"Loaded {len(ios)} IOs: {', '.join(str(io) for io in ios)}") return ios diff --git a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py index 5beb947c..e97e6f96 100644 --- a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py +++ b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py @@ -1,6 +1,9 @@ +from abc import ABC from enum import StrEnum from time import sleep +from carlos.edge.interface.device import AnalogInput, GpioConfig + from carlos.edge.device.protocol import GPIO @@ -107,3 +110,38 @@ def read(self) -> tuple[float, float]: temperature = float(int(data[17:32], 2) * 0.2 * (0.5 - int(data[16], 2))) return humidity, temperature + + +class DHTXX(AnalogInput, ABC): + """DHTXX Temperature and Humidity Sensor.""" + + def __init__(self, config: GpioConfig): + + super().__init__(config=config) + + self._dht: DHT | None = None + self._dht_type: DHTType | None = None + + def setup(self): + """Sets up the DHT11 sensor.""" + + self._dht = DHT(dht_type=self._dht_type, pin=self.config.pin) + + def read(self) -> dict[str, float]: + """Reads the temperature and humidity.""" + + assert self._dht is not None, "The DHT sensor has not been initialized." + + # Reading the DHT sensor is quite unreliable, as the device is not a real-time + # device. Thus, we just try it a couple of times and fail if it does not work. + for i in range(16): + try: + temperature, humidity = self._dht.read() + return { + "temperature": temperature, + "humidity": humidity, + } + except RuntimeError: + pass + + raise RuntimeError(f"Could not read {self._dht_type} sensor.") diff --git a/lib/py_edge_device/carlos/edge/device/io/dht11.py b/lib/py_edge_device/carlos/edge/device/io/dht11.py index 7460d610..5ca094e8 100644 --- a/lib/py_edge_device/carlos/edge/device/io/dht11.py +++ b/lib/py_edge_device/carlos/edge/device/io/dht11.py @@ -1,40 +1,16 @@ -from carlos.edge.interface.device import AnalogInput, GpioConfig, IoFactory +from carlos.edge.interface.device import GpioConfig, IoFactory -from ._dhtxx import DHT, DHTType +from ._dhtxx import DHTXX, DHTType -class DHT11(AnalogInput): +class DHT11(DHTXX): """DHT11 Temperature and Humidity Sensor.""" def __init__(self, config: GpioConfig): super().__init__(config=config) - self._dht: DHT | None = None - - def setup(self): - """Sets up the DHT11 sensor.""" - - self._dht = DHT(dht_type=DHTType.DHT11, pin=self.config.pin) - - def read(self) -> dict[str, float]: - """Reads the temperature and humidity.""" - - assert self._dht is not None, "The DHT sensor has not been initialized." - - # Reading the DHT sensor is quite unreliable, as the device is not a real-time - # device. Thus, we just try it a couple of times and fail if it does not work. - for i in range(16): - try: - temperature, humidity = self._dht.read() - return { - "temperature": temperature, - "humidity": humidity, - } - except RuntimeError: - pass - - raise RuntimeError("Could not read DHT11 sensor.") + self._dht_type = DHTType.DHT11 IoFactory().register(ptype=__name__, config=GpioConfig, factory=DHT11) diff --git a/lib/py_edge_device/carlos/edge/device/io/dht22.py b/lib/py_edge_device/carlos/edge/device/io/dht22.py new file mode 100644 index 00000000..81fdcadf --- /dev/null +++ b/lib/py_edge_device/carlos/edge/device/io/dht22.py @@ -0,0 +1,16 @@ +from carlos.edge.interface.device import GpioConfig, IoFactory + +from ._dhtxx import DHTXX, DHTType + + +class DHT22(DHTXX): + """DHT22 Temperature and Humidity Sensor.""" + + def __init__(self, config: GpioConfig): + + super().__init__(config=config) + + self._dht_type = DHTType.DHT22 + + +IoFactory().register(ptype=__name__, config=GpioConfig, factory=DHT22) diff --git a/lib/py_edge_device/carlos/edge/device/io/relay.py b/lib/py_edge_device/carlos/edge/device/io/relay.py index be0be8a7..dcc3f2b0 100644 --- a/lib/py_edge_device/carlos/edge/device/io/relay.py +++ b/lib/py_edge_device/carlos/edge/device/io/relay.py @@ -1,12 +1,20 @@ +from typing import Literal + from carlos.edge.interface.device import DigitalOutput, GpioConfig, IoFactory +from pydantic import Field from carlos.edge.device.protocol import GPIO +class RelayConfig(GpioConfig): + + direction: Literal["output"] = Field("output") + + class Relay(DigitalOutput): """Relay.""" - def __init__(self, config: GpioConfig): + def __init__(self, config: RelayConfig): super().__init__(config=config) def setup(self): @@ -17,4 +25,4 @@ def set(self, value: bool): GPIO.output(self.config.pin, value) -IoFactory().register(ptype=__name__, config=GpioConfig, factory=Relay) +IoFactory().register(ptype=__name__, config=RelayConfig, factory=Relay) diff --git a/lib/py_edge_device/carlos/edge/device/io/si1145.py b/lib/py_edge_device/carlos/edge/device/io/si1145.py index 8bf3f18a..a01b1674 100644 --- a/lib/py_edge_device/carlos/edge/device/io/si1145.py +++ b/lib/py_edge_device/carlos/edge/device/io/si1145.py @@ -1,13 +1,22 @@ import time +from typing import Literal from carlos.edge.interface.device import AnalogInput, I2cConfig, IoFactory +from pydantic import Field from carlos.edge.device.protocol import I2C +class Si1145Config(I2cConfig): + + direction: Literal["input"] = Field("input") + + address: Literal["0x60"] = Field("0x60") + + class SI1145(AnalogInput): - def __init__(self, config: I2cConfig): + def __init__(self, config: Si1145Config): if config.address_int != SDL_Pi_SI1145.ADDR: raise ValueError( @@ -42,7 +51,7 @@ def read(self) -> dict[str, float]: } -IoFactory().register(ptype=__name__, config=I2cConfig, factory=SI1145) +IoFactory().register(ptype=__name__, config=Si1145Config, factory=SI1145) class SDL_Pi_SI1145: diff --git a/lib/py_edge_device/carlos/edge/device/runtime.py b/lib/py_edge_device/carlos/edge/device/runtime.py index d268cbf8..442f528b 100644 --- a/lib/py_edge_device/carlos/edge/device/runtime.py +++ b/lib/py_edge_device/carlos/edge/device/runtime.py @@ -28,8 +28,6 @@ def __init__(self, device_id: DeviceId, protocol: EdgeProtocol): self.device_id = device_id self.protocol = protocol - self.ios = load_io() - async def run(self): """Runs the device runtime.""" @@ -68,11 +66,15 @@ def _prepare_runtime(self): retention=timedelta(days=60), ) - validate_device_address_space(self.ios) - self._setup_io() +class IoManager: # pragma: no cover - def _setup_io(self): + def __init__(self): + + self.ios = load_io() + validate_device_address_space(self.ios) + + def setup(self): """Sets up the I/O peripherals.""" for io in self.ios: logger.debug( @@ -80,6 +82,14 @@ def _setup_io(self): ) io.setup() + def test(self): + """Tests the I/O peripherals.""" + for io in self.ios: + logger.debug( + f"Testing I/O peripheral {io.config.identifier} ({io.config.module})." + ) + io.test() + async def send_ping( communication_handler: DeviceCommunicationHandler, diff --git a/lib/py_edge_interface/carlos/edge/interface/device/io.py b/lib/py_edge_interface/carlos/edge/interface/device/io.py index 3d70319d..7db22ae0 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/io.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/io.py @@ -8,9 +8,12 @@ import asyncio import concurrent.futures from abc import ABC, abstractmethod +from asyncio import sleep from collections import namedtuple from typing import Any, Callable, Generic, Iterable, TypeVar +from loguru import logger + from .config import GpioConfig, I2cConfig, IoConfig IoConfigTypeVar = TypeVar("IoConfigTypeVar", bound=IoConfig) @@ -31,6 +34,11 @@ def setup(self): is not able to run the setup method of the peripheral outside the device.""" pass + @abstractmethod + def test(self): + """Tests the peripheral. This is used to validate a config by a human.""" + pass + class AnalogInput(CarlosPeripheral, ABC): """Common base class for all analog input peripherals.""" @@ -41,9 +49,16 @@ def read(self) -> dict[str, float]: containing the value of the analog input.""" pass + def test(self): + """Tests the analog input by reading the value.""" + + logger.info(f"Testing {self}") + data = self.read() + logger.info(f"Read data: {data}") + async def read_async(self) -> dict[str, float]: - """Reads the value of the analog input asynchronously. The return value is a dictionary - containing the value of the analog input.""" + """Reads the value of the analog input asynchronously. The return value is a + dictionary containing the value of the analog input.""" loop = asyncio.get_running_loop() @@ -58,6 +73,18 @@ class DigitalOutput(CarlosPeripheral, ABC): def set(self, value: bool): pass + def test(self): + """Tests the digital output by setting the value to False, then True for 1 second, + and then back to False.""" + + logger.info(f"Testing {self}") + self.set(False) + self.set(True) + logger.info(f"Set value to True.") + sleep(1) + self.set(False) + logger.info(f"Set value to False.") + CarlosIO = AnalogInput | DigitalOutput From 71394759199ff75a483556a3009909e19beaadbd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Apr 2024 21:16:15 +0000 Subject: [PATCH 20/57] update lib/py_edge_interface Co-authored-by: flxdot --- lib/py_edge_interface/carlos/edge/interface/device/io.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/io.py b/lib/py_edge_interface/carlos/edge/interface/device/io.py index 7db22ae0..835802ed 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/io.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/io.py @@ -80,10 +80,10 @@ def test(self): logger.info(f"Testing {self}") self.set(False) self.set(True) - logger.info(f"Set value to True.") + logger.info("Set value to True.") sleep(1) self.set(False) - logger.info(f"Set value to False.") + logger.info("Set value to False.") CarlosIO = AnalogInput | DigitalOutput From de5f7bdb63b20b44161ec1da4462d99a94af4858 Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 23:22:41 +0200 Subject: [PATCH 21/57] add cli command to test io config --- .../carlos/edge/device/runtime.py | 17 +++++++++++++++-- services/device/device/cli/config.py | 8 ++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/runtime.py b/lib/py_edge_device/carlos/edge/device/runtime.py index 442f528b..79660ff5 100644 --- a/lib/py_edge_device/carlos/edge/device/runtime.py +++ b/lib/py_edge_device/carlos/edge/device/runtime.py @@ -3,6 +3,7 @@ from datetime import timedelta from pathlib import Path +from typing import Self from apscheduler import AsyncScheduler from apscheduler.triggers.interval import IntervalTrigger @@ -28,6 +29,8 @@ def __init__(self, device_id: DeviceId, protocol: EdgeProtocol): self.device_id = device_id self.protocol = protocol + self.io_manager = IoManager() + async def run(self): """Runs the device runtime.""" @@ -46,6 +49,7 @@ async def run(self): kwargs={"communication_handler": communication_handler}, trigger=IntervalTrigger(minutes=1), ) + self.io_manager.register_tasks(scheduler=scheduler) await scheduler.start_in_background() while True: @@ -74,7 +78,7 @@ def __init__(self): self.ios = load_io() validate_device_address_space(self.ios) - def setup(self): + def setup(self) -> Self: """Sets up the I/O peripherals.""" for io in self.ios: logger.debug( @@ -82,7 +86,14 @@ def setup(self): ) io.setup() - def test(self): + return self + + def register_tasks(self, scheduler: AsyncScheduler) -> Self: + """Registers the tasks of the I/O peripherals.""" + + return self + + def test(self) -> Self: """Tests the I/O peripherals.""" for io in self.ios: logger.debug( @@ -90,6 +101,8 @@ def test(self): ) io.test() + return self + async def send_ping( communication_handler: DeviceCommunicationHandler, diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index 70946016..6253b5e4 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -3,6 +3,7 @@ from typing import TypeVar import typer +from carlos.edge.device.runtime import IoManager from pydantic import BaseModel from pydantic_core import PydanticUndefinedType from rich import print, print_json @@ -54,3 +55,10 @@ def show(): print("\n[bold]Connection[/bold] configuration:") print_json(read_connection_settings().model_dump_json()) + + +@config_cli.command() +def test(): # pragma: no cover + """Tests the io peripherals.""" + + IoManager().setup().test() From 48aff5df87a19238a3a70558860117931ad07827 Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 23:30:10 +0200 Subject: [PATCH 22/57] fix some bugs --- .../carlos/edge/interface/device/io.py | 2 +- services/device/poetry.lock | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/io.py b/lib/py_edge_interface/carlos/edge/interface/device/io.py index 835802ed..f5d18c63 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/io.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/io.py @@ -26,7 +26,7 @@ def __init__(self, config: IoConfigTypeVar): self.config: IoConfigTypeVar = config def __str__(self): - return f"{self.config.identifier} ({self.config.module})" + return f"{self.config.identifier} ({self.config.driver})" @abstractmethod def setup(self): diff --git a/services/device/poetry.lock b/services/device/poetry.lock index b76f5872..ff3f1b7d 100644 --- a/services/device/poetry.lock +++ b/services/device/poetry.lock @@ -134,9 +134,12 @@ develop = false apscheduler = "^4.0.0a4" "carlos.edge.interface" = {path = "../py_edge_interface"} loguru = "^0.7.2" +psutil = "^5.9.8" pydantic = "^2.6.4" pyyaml = "^6.0.1" +rpi-gpio = {version = "^0.7.1", markers = "platform_machine == \"armv7l\" or platform_machine == \"aarch64\""} semver = "^3.0.2" +smbus2 = "^0.4.3" [package.source] type = "directory" @@ -749,6 +752,34 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "psutil" +version = "5.9.8" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, + {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, + {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, + {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, + {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, + {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, + {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, + {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, + {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, + {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + [[package]] name = "pydantic" version = "2.7.0" @@ -1085,6 +1116,21 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "rpi-gpio" +version = "0.7.1" +description = "A module to control Raspberry Pi GPIO channels" +optional = false +python-versions = "*" +files = [ + {file = "RPi.GPIO-0.7.1-cp27-cp27mu-linux_armv6l.whl", hash = "sha256:b86b66dc02faa5461b443a1e1f0c1d209d64ab5229696f32fb3b0215e0600c8c"}, + {file = "RPi.GPIO-0.7.1-cp310-cp310-linux_armv6l.whl", hash = "sha256:57b6c044ef5375a78c8dda27cdfadf329e76aa6943cd6cffbbbd345a9adf9ca5"}, + {file = "RPi.GPIO-0.7.1-cp37-cp37m-linux_armv6l.whl", hash = "sha256:77afb817b81331ce3049a4b8f94a85e41b7c404d8e56b61ac0f1eb75c3120868"}, + {file = "RPi.GPIO-0.7.1-cp38-cp38-linux_armv6l.whl", hash = "sha256:29226823da8b5ccb9001d795a944f2e00924eeae583490f0bc7317581172c624"}, + {file = "RPi.GPIO-0.7.1-cp39-cp39-linux_armv6l.whl", hash = "sha256:15311d3b063b71dee738cd26570effc9985a952454d162937c34e08c0fc99902"}, + {file = "RPi.GPIO-0.7.1.tar.gz", hash = "sha256:cd61c4b03c37b62bba4a5acfea9862749c33c618e0295e7e90aa4713fb373b70"}, +] + [[package]] name = "ruff" version = "0.3.7" @@ -1133,6 +1179,22 @@ files = [ {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] +[[package]] +name = "smbus2" +version = "0.4.3" +description = "smbus2 is a drop-in replacement for smbus-cffi/smbus-python in pure Python" +optional = false +python-versions = "*" +files = [ + {file = "smbus2-0.4.3-py2.py3-none-any.whl", hash = "sha256:a2fc29cfda4081ead2ed61ef2c4fc041d71dd40a8d917e85216f44786fca2d1d"}, + {file = "smbus2-0.4.3.tar.gz", hash = "sha256:36f2288a8e1a363cb7a7b2244ec98d880eb5a728a2494ac9c71e9de7bf6a803a"}, +] + +[package.extras] +docs = ["sphinx (>=1.5.3)"] +qa = ["flake8"] +test = ["mock", "nose"] + [[package]] name = "sniffio" version = "1.3.1" From 3ef70c5411e3e1d0cea912a84b249157f0444f07 Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 23:32:19 +0200 Subject: [PATCH 23/57] fix some bugs --- lib/py_edge_device/carlos/edge/device/runtime.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/runtime.py b/lib/py_edge_device/carlos/edge/device/runtime.py index 79660ff5..8f00973c 100644 --- a/lib/py_edge_device/carlos/edge/device/runtime.py +++ b/lib/py_edge_device/carlos/edge/device/runtime.py @@ -81,9 +81,7 @@ def __init__(self): def setup(self) -> Self: """Sets up the I/O peripherals.""" for io in self.ios: - logger.debug( - f"Setting up I/O peripheral {io.config.identifier} ({io.config.module})." - ) + logger.debug(f"Setting up I/O peripheral {io}.") io.setup() return self @@ -96,9 +94,7 @@ def register_tasks(self, scheduler: AsyncScheduler) -> Self: def test(self) -> Self: """Tests the I/O peripherals.""" for io in self.ios: - logger.debug( - f"Testing I/O peripheral {io.config.identifier} ({io.config.module})." - ) + logger.debug(f"Testing I/O peripheral {io}.") io.test() return self From 72b98629dd719dfb0ef240757b8691d0e595bddf Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 23:47:58 +0200 Subject: [PATCH 24/57] fix some bugs and improve interface --- lib/py_edge_device/carlos/edge/device/io/relay.py | 2 +- lib/py_edge_device/carlos/edge/device/runtime.py | 8 -------- .../carlos/edge/interface/device/io.py | 9 +-------- services/device/device/cli/config.py | 13 ++++++++++++- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/io/relay.py b/lib/py_edge_device/carlos/edge/device/io/relay.py index dcc3f2b0..027d3897 100644 --- a/lib/py_edge_device/carlos/edge/device/io/relay.py +++ b/lib/py_edge_device/carlos/edge/device/io/relay.py @@ -18,7 +18,7 @@ def __init__(self, config: RelayConfig): super().__init__(config=config) def setup(self): - GPIO.setup(self.config.pin, GPIO.OUT) + GPIO.setup(self.config.pin, GPIO.OUT, initial=GPIO.LOW) def set(self, value: bool): """Writes the value to the relay.""" diff --git a/lib/py_edge_device/carlos/edge/device/runtime.py b/lib/py_edge_device/carlos/edge/device/runtime.py index 8f00973c..64487b3b 100644 --- a/lib/py_edge_device/carlos/edge/device/runtime.py +++ b/lib/py_edge_device/carlos/edge/device/runtime.py @@ -91,14 +91,6 @@ def register_tasks(self, scheduler: AsyncScheduler) -> Self: return self - def test(self) -> Self: - """Tests the I/O peripherals.""" - for io in self.ios: - logger.debug(f"Testing I/O peripheral {io}.") - io.test() - - return self - async def send_ping( communication_handler: DeviceCommunicationHandler, diff --git a/lib/py_edge_interface/carlos/edge/interface/device/io.py b/lib/py_edge_interface/carlos/edge/interface/device/io.py index f5d18c63..97ec87a0 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/io.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/io.py @@ -12,8 +12,6 @@ from collections import namedtuple from typing import Any, Callable, Generic, Iterable, TypeVar -from loguru import logger - from .config import GpioConfig, I2cConfig, IoConfig IoConfigTypeVar = TypeVar("IoConfigTypeVar", bound=IoConfig) @@ -52,9 +50,7 @@ def read(self) -> dict[str, float]: def test(self): """Tests the analog input by reading the value.""" - logger.info(f"Testing {self}") - data = self.read() - logger.info(f"Read data: {data}") + return self.read() async def read_async(self) -> dict[str, float]: """Reads the value of the analog input asynchronously. The return value is a @@ -77,13 +73,10 @@ def test(self): """Tests the digital output by setting the value to False, then True for 1 second, and then back to False.""" - logger.info(f"Testing {self}") self.set(False) self.set(True) - logger.info("Set value to True.") sleep(1) self.set(False) - logger.info("Set value to False.") CarlosIO = AnalogInput | DigitalOutput diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index 6253b5e4..bacd2349 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -7,6 +7,7 @@ from pydantic import BaseModel from pydantic_core import PydanticUndefinedType from rich import print, print_json +from rich.console import Console from device.connection import ( ConnectionSettings, @@ -14,6 +15,8 @@ write_connection_settings, ) +console = Console() + config_cli = typer.Typer() @@ -61,4 +64,12 @@ def show(): def test(): # pragma: no cover """Tests the io peripherals.""" - IoManager().setup().test() + for io in IoManager().setup().ios: + console.log(f"[blue]Testing {io} ... ", end="") + try: + result = io.test() + console.log("[green]passed") + console.log(result) + except Exception as e: + console.log("[red]failed") + console.log(e) From e9cc2b4faa0d6d14ea22f7651040c1db2b531108 Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 23:49:35 +0200 Subject: [PATCH 25/57] better logging --- lib/py_edge_interface/carlos/edge/interface/device/io.py | 4 ++++ services/device/device/cli/config.py | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/io.py b/lib/py_edge_interface/carlos/edge/interface/device/io.py index 97ec87a0..90c9f6b9 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/io.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/io.py @@ -26,6 +26,10 @@ def __init__(self, config: IoConfigTypeVar): def __str__(self): return f"{self.config.identifier} ({self.config.driver})" + @property + def identifier(self): + return self.config.identifier + @abstractmethod def setup(self): """Sets up the peripheral. This is required for testing. As the test runner diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index bacd2349..f3b0054a 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -64,6 +64,7 @@ def show(): def test(): # pragma: no cover """Tests the io peripherals.""" + exceptions = {} for io in IoManager().setup().ios: console.log(f"[blue]Testing {io} ... ", end="") try: @@ -72,4 +73,9 @@ def test(): # pragma: no cover console.log(result) except Exception as e: console.log("[red]failed") - console.log(e) + exceptions[io.identifier] = e + + if exceptions: + console.log("\n[red]The following IO peripherals failed:") + for identifier, exception in exceptions.items(): + console.log(f"[red]{identifier}: {exception}") From 69a9b31cf04c2cdd96b4649eef006f8a48b1d162 Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 23:51:19 +0200 Subject: [PATCH 26/57] wrong sleep --- lib/py_edge_interface/carlos/edge/interface/device/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/io.py b/lib/py_edge_interface/carlos/edge/interface/device/io.py index 90c9f6b9..6eff9cd8 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/io.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/io.py @@ -8,8 +8,8 @@ import asyncio import concurrent.futures from abc import ABC, abstractmethod -from asyncio import sleep from collections import namedtuple +from time import sleep from typing import Any, Callable, Generic, Iterable, TypeVar from .config import GpioConfig, I2cConfig, IoConfig From 2b3771a14acfa832f13ef62a6faae0c8551de537 Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 23:52:37 +0200 Subject: [PATCH 27/57] better test logging --- services/device/device/cli/config.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index f3b0054a..3a43b6bb 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -65,16 +65,23 @@ def test(): # pragma: no cover """Tests the io peripherals.""" exceptions = {} + results = {} for io in IoManager().setup().ios: console.log(f"[blue]Testing {io} ... ", end="") try: result = io.test() console.log("[green]passed") - console.log(result) + if result: + results[io.identifier] = result except Exception as e: console.log("[red]failed") exceptions[io.identifier] = e + if results: + console.log("\n[blue]The following IO peripherals returned data:") + for identifier, result in results.items(): + console.log(f"[blue]{identifier}: {result}") + if exceptions: console.log("\n[red]The following IO peripherals failed:") for identifier, exception in exceptions.items(): From 5542ec71d312cc69523c2d8b4a509166bc36bc92 Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 23:55:26 +0200 Subject: [PATCH 28/57] other stuff --- services/device/device/cli/config.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index 3a43b6bb..9aca5e6f 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -67,22 +67,22 @@ def test(): # pragma: no cover exceptions = {} results = {} for io in IoManager().setup().ios: - console.log(f"[blue]Testing {io} ... ", end="") + console.print(f"[cyan]Testing {io} ... ", end="") try: result = io.test() - console.log("[green]passed") + console.print("[green]passed") if result: results[io.identifier] = result except Exception as e: - console.log("[red]failed") + console.print("[red]failed") exceptions[io.identifier] = e if results: - console.log("\n[blue]The following IO peripherals returned data:") + console.print("\n[cyan]The following IO peripherals returned data:") for identifier, result in results.items(): - console.log(f"[blue]{identifier}: {result}") + console.print(f"[cyan]{identifier}: {result}") if exceptions: - console.log("\n[red]The following IO peripherals failed:") + console.print("\n[red]The following IO peripherals failed:") for identifier, exception in exceptions.items(): - console.log(f"[red]{identifier}: {exception}") + console.print(f"[red]{identifier}: {exception}") From b0cfd74b6c355a20fb49cb90774e888f04fefdb4 Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 23:57:22 +0200 Subject: [PATCH 29/57] traceback --- services/device/device/cli/config.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index 9aca5e6f..d3a91870 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -1,5 +1,6 @@ from __future__ import print_function, unicode_literals +import traceback from typing import TypeVar import typer @@ -67,22 +68,22 @@ def test(): # pragma: no cover exceptions = {} results = {} for io in IoManager().setup().ios: - console.print(f"[cyan]Testing {io} ... ", end="") + console.print(f"[cyan]Testing {io} ... [/cyan]", end="") try: result = io.test() - console.print("[green]passed") + console.print("[green]passed[/green]") if result: results[io.identifier] = result except Exception as e: - console.print("[red]failed") + console.print("[red]failed[/red]") exceptions[io.identifier] = e if results: - console.print("\n[cyan]The following IO peripherals returned data:") + console.print("\nThe following IO peripherals returned data:") for identifier, result in results.items(): - console.print(f"[cyan]{identifier}: {result}") + console.print(f"{identifier}: {result}") if exceptions: - console.print("\n[red]The following IO peripherals failed:") + console.print("\nThe following IO peripherals [red]failed[/red]:") for identifier, exception in exceptions.items(): - console.print(f"[red]{identifier}: {exception}") + console.print(f"[red]{identifier}: {traceback.format_exception(exception)}") From ae575068e2b9520a31729974346566d0a08653f5 Mon Sep 17 00:00:00 2001 From: flxdot Date: Thu, 18 Apr 2024 23:57:36 +0200 Subject: [PATCH 30/57] reset color --- services/device/device/cli/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index d3a91870..4c800345 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -86,4 +86,4 @@ def test(): # pragma: no cover if exceptions: console.print("\nThe following IO peripherals [red]failed[/red]:") for identifier, exception in exceptions.items(): - console.print(f"[red]{identifier}: {traceback.format_exception(exception)}") + console.print(f"[red]{identifier}[/red]: {traceback.format_exception(exception)}") From 5cc8d9c823ccf619c10278a6e54f455092e15450 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Apr 2024 21:58:33 +0000 Subject: [PATCH 31/57] update services/device Co-authored-by: flxdot --- services/device/device/cli/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index 4c800345..f0264fd1 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -86,4 +86,6 @@ def test(): # pragma: no cover if exceptions: console.print("\nThe following IO peripherals [red]failed[/red]:") for identifier, exception in exceptions.items(): - console.print(f"[red]{identifier}[/red]: {traceback.format_exception(exception)}") + console.print( + f"[red]{identifier}[/red]: {traceback.format_exception(exception)}" + ) From 41920731a314767d50a2312b251b801c326867c1 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 00:02:53 +0200 Subject: [PATCH 32/57] try --- services/device/device/cli/config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index 4c800345..5d62d9d6 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -81,9 +81,11 @@ def test(): # pragma: no cover if results: console.print("\nThe following IO peripherals returned data:") for identifier, result in results.items(): - console.print(f"{identifier}: {result}") + console.print(f"{identifier}:") + console.print(result) if exceptions: console.print("\nThe following IO peripherals [red]failed[/red]:") for identifier, exception in exceptions.items(): - console.print(f"[red]{identifier}[/red]: {traceback.format_exception(exception)}") + console.print(f"[red]{identifier}[/red]:") + console.print(exception) From 8fa91e9b38b3159401dc25ffec8f10121eb42294 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 00:03:21 +0200 Subject: [PATCH 33/57] import --- services/device/device/cli/config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index 5d62d9d6..4b851e9a 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -1,6 +1,5 @@ from __future__ import print_function, unicode_literals -import traceback from typing import TypeVar import typer From 662fae283862edb1791996605810f77f34ecac18 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 00:04:29 +0200 Subject: [PATCH 34/57] local print --- services/device/device/cli/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index 4b851e9a..a277f02e 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -75,6 +75,7 @@ def test(): # pragma: no cover results[io.identifier] = result except Exception as e: console.print("[red]failed[/red]") + console.print_exception() exceptions[io.identifier] = e if results: From f358a383292db8e0c0f4b39adb1216944ed94af3 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 00:06:41 +0200 Subject: [PATCH 35/57] better error messages --- lib/py_edge_device/carlos/edge/device/io/_dhtxx.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py index e97e6f96..c50890d2 100644 --- a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py +++ b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py @@ -134,6 +134,7 @@ def read(self) -> dict[str, float]: # Reading the DHT sensor is quite unreliable, as the device is not a real-time # device. Thus, we just try it a couple of times and fail if it does not work. + last_error = None for i in range(16): try: temperature, humidity = self._dht.read() @@ -141,7 +142,7 @@ def read(self) -> dict[str, float]: "temperature": temperature, "humidity": humidity, } - except RuntimeError: - pass + except RuntimeError as ex: + last_error = ex - raise RuntimeError(f"Could not read {self._dht_type} sensor.") + raise last_error From 44f52a4c76cb4e3e24e352bbc74a787697ce2d93 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 00:11:57 +0200 Subject: [PATCH 36/57] fix bug with output --- lib/py_edge_device/carlos/edge/device/io/_dhtxx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py index c50890d2..e3b1ffce 100644 --- a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py +++ b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py @@ -109,7 +109,7 @@ def read(self) -> tuple[float, float]: humidity = float(int(data[0:16], 2) * 0.1) temperature = float(int(data[17:32], 2) * 0.2 * (0.5 - int(data[16], 2))) - return humidity, temperature + return temperature, humidity class DHTXX(AnalogInput, ABC): From 01908283b60873a677827b4eb6aeab08e6b98928 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 00:13:14 +0200 Subject: [PATCH 37/57] fix test config --- lib/py_edge_device/tests/test_data/device_config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/py_edge_device/tests/test_data/device_config b/lib/py_edge_device/tests/test_data/device_config index 88a1b7f8..79c41fc3 100644 --- a/lib/py_edge_device/tests/test_data/device_config +++ b/lib/py_edge_device/tests/test_data/device_config @@ -3,7 +3,7 @@ io: driver: dht11 direction: input protocol: gpio - pin: 5 + pin: 4 - identifier: uv-light driver: si1145 direction: input From dfd70610ba7a294547ce25f5149d1f90783a9478 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 00:15:49 +0200 Subject: [PATCH 38/57] rename some stuff --- lib/py_edge_device/tests/test_data/device_config | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/py_edge_device/tests/test_data/device_config b/lib/py_edge_device/tests/test_data/device_config index 79c41fc3..180bc21a 100644 --- a/lib/py_edge_device/tests/test_data/device_config +++ b/lib/py_edge_device/tests/test_data/device_config @@ -34,17 +34,17 @@ io: direction: output protocol: gpio pin: 24 - - identifier: spare-1 + - identifier: relay-6 driver: carlos.edge.device.io.relay direction: output protocol: gpio pin: 25 - - identifier: spare-2 + - identifier: relay-7 driver: carlos.edge.device.io.relay direction: output protocol: gpio pin: 26 - - identifier: spare-3 + - identifier: relay-8 driver: carlos.edge.device.io.relay direction: output protocol: gpio From 27eb83b52cb08e279ab4987fa494b0a18365f3eb Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 08:52:10 +0200 Subject: [PATCH 39/57] minify config --- .../tests/test_data/device_config | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/lib/py_edge_device/tests/test_data/device_config b/lib/py_edge_device/tests/test_data/device_config index 180bc21a..75a7d0b4 100644 --- a/lib/py_edge_device/tests/test_data/device_config +++ b/lib/py_edge_device/tests/test_data/device_config @@ -1,51 +1,30 @@ io: - identifier: temp-humi-intern driver: dht11 - direction: input - protocol: gpio pin: 4 - identifier: uv-light driver: si1145 - direction: input - protocol: i2c - address: 0x60 - identifier: pump driver: relay - direction: output - protocol: gpio pin: 20 - identifier: valve-1 driver: relay - direction: output - protocol: gpio pin: 21 - identifier: valve-2 driver: relay - direction: output - protocol: gpio pin: 22 - identifier: valve-3 driver: relay - direction: output - protocol: gpio pin: 23 - identifier: valve-4 driver: carlos.edge.device.io.relay - direction: output - protocol: gpio pin: 24 - identifier: relay-6 driver: carlos.edge.device.io.relay - direction: output - protocol: gpio pin: 25 - identifier: relay-7 driver: carlos.edge.device.io.relay - direction: output - protocol: gpio pin: 26 - identifier: relay-8 driver: carlos.edge.device.io.relay - direction: output - protocol: gpio pin: 27 From 46113275d54403175c903b7071e5ff6874595b9b Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 17:12:25 +0200 Subject: [PATCH 40/57] fix device ci --- lib/py_edge_device/carlos/edge/device/io/_dhtxx.py | 13 ++++++++++++- lib/py_edge_device/carlos/edge/device/io/dht11.py | 4 ++-- lib/py_edge_device/carlos/edge/device/io/dht22.py | 4 ++-- lib/py_edge_device/tests/test_data/__init__.py | 3 ++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py index e3b1ffce..2aa89ee6 100644 --- a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py +++ b/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py @@ -1,12 +1,22 @@ +__all__ = ["DHTXX", "DhtConfig", "DHTType"] + from abc import ABC from enum import StrEnum from time import sleep +from typing import Literal from carlos.edge.interface.device import AnalogInput, GpioConfig +from pydantic import Field from carlos.edge.device.protocol import GPIO +class DhtConfig(GpioConfig): + """Configuration for a DHT sensor.""" + + direction: Literal["input"] = Field("input") + + class DHTType(StrEnum): DHT11 = "DHT11" DHT22 = "DHT22" @@ -134,7 +144,7 @@ def read(self) -> dict[str, float]: # Reading the DHT sensor is quite unreliable, as the device is not a real-time # device. Thus, we just try it a couple of times and fail if it does not work. - last_error = None + last_error: Exception | None = None for i in range(16): try: temperature, humidity = self._dht.read() @@ -145,4 +155,5 @@ def read(self) -> dict[str, float]: except RuntimeError as ex: last_error = ex + assert last_error is not None raise last_error diff --git a/lib/py_edge_device/carlos/edge/device/io/dht11.py b/lib/py_edge_device/carlos/edge/device/io/dht11.py index 5ca094e8..62e033b8 100644 --- a/lib/py_edge_device/carlos/edge/device/io/dht11.py +++ b/lib/py_edge_device/carlos/edge/device/io/dht11.py @@ -1,6 +1,6 @@ from carlos.edge.interface.device import GpioConfig, IoFactory -from ._dhtxx import DHTXX, DHTType +from ._dhtxx import DHTXX, DhtConfig, DHTType class DHT11(DHTXX): @@ -13,4 +13,4 @@ def __init__(self, config: GpioConfig): self._dht_type = DHTType.DHT11 -IoFactory().register(ptype=__name__, config=GpioConfig, factory=DHT11) +IoFactory().register(ptype=__name__, config=DhtConfig, factory=DHT11) diff --git a/lib/py_edge_device/carlos/edge/device/io/dht22.py b/lib/py_edge_device/carlos/edge/device/io/dht22.py index 81fdcadf..2052ca36 100644 --- a/lib/py_edge_device/carlos/edge/device/io/dht22.py +++ b/lib/py_edge_device/carlos/edge/device/io/dht22.py @@ -1,6 +1,6 @@ from carlos.edge.interface.device import GpioConfig, IoFactory -from ._dhtxx import DHTXX, DHTType +from ._dhtxx import DHTXX, DhtConfig, DHTType class DHT22(DHTXX): @@ -13,4 +13,4 @@ def __init__(self, config: GpioConfig): self._dht_type = DHTType.DHT22 -IoFactory().register(ptype=__name__, config=GpioConfig, factory=DHT22) +IoFactory().register(ptype=__name__, config=DhtConfig, factory=DHT22) diff --git a/lib/py_edge_device/tests/test_data/__init__.py b/lib/py_edge_device/tests/test_data/__init__.py index 146715dd..999b0f15 100644 --- a/lib/py_edge_device/tests/test_data/__init__.py +++ b/lib/py_edge_device/tests/test_data/__init__.py @@ -2,4 +2,5 @@ TEST_DEVICE_WORKDIR = Path(__file__).parent -EXPECTED_IO_COUNT = 10 +EXPECTED_IO_COUNT = 11 +"""10 configured I/Os + 1 default I/O.""" From 12fcd97cad371bad82a66a49a20141c240a01f87 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 17:13:35 +0200 Subject: [PATCH 41/57] fix services/device --- services/device/device/run.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/device/device/run.py b/services/device/device/run.py index 12f6cde4..37fede57 100644 --- a/services/device/device/run.py +++ b/services/device/device/run.py @@ -1,4 +1,4 @@ -from carlos.edge.device import DeviceRuntime, read_config +from carlos.edge.device import DeviceRuntime from carlos.edge.device.constants import VERSION from loguru import logger @@ -24,12 +24,11 @@ async def main(): # pragma: no cover logger.info(f"Starting Carlos device (v{VERSION})...") - device_config = read_config() device_connection = read_connection_settings() protocol = DeviceWebsocketClient(settings=device_connection) runtime = DeviceRuntime( - config=device_config, + device_id=device_connection.device_id, protocol=protocol, ) await runtime.run() From 3dcb5a9acc5ad60a9d16cf4997596b4fac1709e6 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 17:33:16 +0200 Subject: [PATCH 42/57] improve naming --- .../carlos/edge/device/config.py | 17 +++-- .../carlos/edge/device/config_test.py | 10 +-- .../edge/device/{io => driver}/__init__.py | 0 .../edge/device/{io => driver}/_dhtxx.py | 6 +- .../device/{io => driver}/device_metrics.py | 6 +- .../edge/device/{io => driver}/dht11.py | 6 +- .../edge/device/{io => driver}/dht22.py | 6 +- .../edge/device/{io => driver}/relay.py | 6 +- .../edge/device/{io => driver}/si1145.py | 14 ++-- .../carlos/edge/device/runtime.py | 2 +- lib/py_edge_device/pyproject.toml | 2 +- .../tests/test_data/device_config | 20 ++--- .../carlos/edge/interface/device/__init__.py | 14 ++-- .../interface/device/{io.py => driver.py} | 76 ++++++++++--------- .../device/{config.py => driver_config.py} | 30 ++++---- 15 files changed, 112 insertions(+), 103 deletions(-) rename lib/py_edge_device/carlos/edge/device/{io => driver}/__init__.py (100%) rename lib/py_edge_device/carlos/edge/device/{io => driver}/_dhtxx.py (96%) rename lib/py_edge_device/carlos/edge/device/{io => driver}/device_metrics.py (79%) rename lib/py_edge_device/carlos/edge/device/{io => driver}/dht11.py (50%) rename lib/py_edge_device/carlos/edge/device/{io => driver}/dht22.py (50%) rename lib/py_edge_device/carlos/edge/device/{io => driver}/relay.py (71%) rename lib/py_edge_device/carlos/edge/device/{io => driver}/si1145.py (97%) rename lib/py_edge_interface/carlos/edge/interface/device/{io.py => driver.py} (71%) rename lib/py_edge_interface/carlos/edge/interface/device/{config.py => driver_config.py} (79%) diff --git a/lib/py_edge_device/carlos/edge/device/config.py b/lib/py_edge_device/carlos/edge/device/config.py index 7fa84407..8fcfcdb9 100644 --- a/lib/py_edge_device/carlos/edge/device/config.py +++ b/lib/py_edge_device/carlos/edge/device/config.py @@ -11,12 +11,12 @@ from typing import TypeVar import yaml -from carlos.edge.interface.device import CarlosIO, IoConfig, IoFactory +from carlos.edge.interface.device import CarlosDriver, DriverConfig, DriverFactory from loguru import logger from pydantic import BaseModel from carlos.edge.device.constants import CONFIG_FILE_NAME -from carlos.edge.device.io.device_metrics import DeviceMetrics +from carlos.edge.device.driver.device_metrics import DeviceMetrics Config = TypeVar("Config", bound=BaseModel) @@ -43,24 +43,25 @@ def write_config_file(path: Path, config: Config): ) -def load_io(config_dir: Path | None = None) -> list[CarlosIO]: +def load_io(config_dir: Path | None = None) -> list[CarlosDriver]: """Reads the configuration from the default location.""" config_dir = config_dir or Path.cwd() with open(config_dir / CONFIG_FILE_NAME, "r") as file: raw_config = yaml.safe_load(file) - io_factory = IoFactory() + factory = DriverFactory() - ios = [io_factory.build(config) for config in raw_config.get("io", [])] + ios = [factory.build(config) for config in raw_config.get("io", [])] # We always want to have some device metrics if not any(isinstance(io, DeviceMetrics) for io in ios): ios.insert( 0, - io_factory.build( - IoConfig( - identifier="__device_metrics__", driver=DeviceMetrics.__module__ + factory.build( + DriverConfig( + identifier="__device_metrics__", + driver_module=DeviceMetrics.__module__, ).model_dump() ), ) diff --git a/lib/py_edge_device/carlos/edge/device/config_test.py b/lib/py_edge_device/carlos/edge/device/config_test.py index fac73446..e772c395 100644 --- a/lib/py_edge_device/carlos/edge/device/config_test.py +++ b/lib/py_edge_device/carlos/edge/device/config_test.py @@ -1,7 +1,7 @@ from pathlib import Path import pytest -from carlos.edge.interface.device import AnalogInput, DigitalOutput, GpioConfig +from carlos.edge.interface.device import AnalogInput, DigitalOutput, GpioDriverConfig from carlos.edge.device.config import load_io, read_config_file, write_config_file from tests.test_data import EXPECTED_IO_COUNT, TEST_DEVICE_WORKDIR @@ -13,18 +13,18 @@ def test_config_file_io(tmp_path: Path): cfg_path = tmp_path / "config" with pytest.raises(FileNotFoundError): - read_config_file(cfg_path, GpioConfig) + read_config_file(cfg_path, GpioDriverConfig) - config = GpioConfig( + config = GpioDriverConfig( identifier="test-config-file-io", - driver="carlos.edge.device.io.dht11", + driver_module="carlos.edge.device.driver.dht11", direction="input", pin=7, ) write_config_file(cfg_path, config) - assert read_config_file(cfg_path, GpioConfig) == config + assert read_config_file(cfg_path, GpioDriverConfig) == config def test_load_io(): diff --git a/lib/py_edge_device/carlos/edge/device/io/__init__.py b/lib/py_edge_device/carlos/edge/device/driver/__init__.py similarity index 100% rename from lib/py_edge_device/carlos/edge/device/io/__init__.py rename to lib/py_edge_device/carlos/edge/device/driver/__init__.py diff --git a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py b/lib/py_edge_device/carlos/edge/device/driver/_dhtxx.py similarity index 96% rename from lib/py_edge_device/carlos/edge/device/io/_dhtxx.py rename to lib/py_edge_device/carlos/edge/device/driver/_dhtxx.py index 2aa89ee6..38fce119 100644 --- a/lib/py_edge_device/carlos/edge/device/io/_dhtxx.py +++ b/lib/py_edge_device/carlos/edge/device/driver/_dhtxx.py @@ -5,13 +5,13 @@ from time import sleep from typing import Literal -from carlos.edge.interface.device import AnalogInput, GpioConfig +from carlos.edge.interface.device import AnalogInput, GpioDriverConfig from pydantic import Field from carlos.edge.device.protocol import GPIO -class DhtConfig(GpioConfig): +class DhtConfig(GpioDriverConfig): """Configuration for a DHT sensor.""" direction: Literal["input"] = Field("input") @@ -125,7 +125,7 @@ def read(self) -> tuple[float, float]: class DHTXX(AnalogInput, ABC): """DHTXX Temperature and Humidity Sensor.""" - def __init__(self, config: GpioConfig): + def __init__(self, config: GpioDriverConfig): super().__init__(config=config) diff --git a/lib/py_edge_device/carlos/edge/device/io/device_metrics.py b/lib/py_edge_device/carlos/edge/device/driver/device_metrics.py similarity index 79% rename from lib/py_edge_device/carlos/edge/device/io/device_metrics.py rename to lib/py_edge_device/carlos/edge/device/driver/device_metrics.py index ed6ed79f..9efa7011 100644 --- a/lib/py_edge_device/carlos/edge/device/io/device_metrics.py +++ b/lib/py_edge_device/carlos/edge/device/driver/device_metrics.py @@ -1,11 +1,11 @@ import psutil -from carlos.edge.interface.device import AnalogInput, IoConfig, IoFactory +from carlos.edge.interface.device import AnalogInput, DriverConfig, DriverFactory class DeviceMetrics(AnalogInput): """Provides the metrics of the device.""" - def __init__(self, config: IoConfig): + def __init__(self, config: DriverConfig): super().__init__(config=config) @@ -32,4 +32,4 @@ def _read_cpu_temp() -> float: return 0.0 -IoFactory().register(ptype=__name__, config=IoConfig, factory=DeviceMetrics) +DriverFactory().register(ptype=__name__, config=DriverConfig, factory=DeviceMetrics) diff --git a/lib/py_edge_device/carlos/edge/device/io/dht11.py b/lib/py_edge_device/carlos/edge/device/driver/dht11.py similarity index 50% rename from lib/py_edge_device/carlos/edge/device/io/dht11.py rename to lib/py_edge_device/carlos/edge/device/driver/dht11.py index 62e033b8..d6c7f9ba 100644 --- a/lib/py_edge_device/carlos/edge/device/io/dht11.py +++ b/lib/py_edge_device/carlos/edge/device/driver/dht11.py @@ -1,4 +1,4 @@ -from carlos.edge.interface.device import GpioConfig, IoFactory +from carlos.edge.interface.device import DriverFactory, GpioDriverConfig from ._dhtxx import DHTXX, DhtConfig, DHTType @@ -6,11 +6,11 @@ class DHT11(DHTXX): """DHT11 Temperature and Humidity Sensor.""" - def __init__(self, config: GpioConfig): + def __init__(self, config: GpioDriverConfig): super().__init__(config=config) self._dht_type = DHTType.DHT11 -IoFactory().register(ptype=__name__, config=DhtConfig, factory=DHT11) +DriverFactory().register(ptype=__name__, config=DhtConfig, factory=DHT11) diff --git a/lib/py_edge_device/carlos/edge/device/io/dht22.py b/lib/py_edge_device/carlos/edge/device/driver/dht22.py similarity index 50% rename from lib/py_edge_device/carlos/edge/device/io/dht22.py rename to lib/py_edge_device/carlos/edge/device/driver/dht22.py index 2052ca36..e16b3e4d 100644 --- a/lib/py_edge_device/carlos/edge/device/io/dht22.py +++ b/lib/py_edge_device/carlos/edge/device/driver/dht22.py @@ -1,4 +1,4 @@ -from carlos.edge.interface.device import GpioConfig, IoFactory +from carlos.edge.interface.device import DriverFactory, GpioDriverConfig from ._dhtxx import DHTXX, DhtConfig, DHTType @@ -6,11 +6,11 @@ class DHT22(DHTXX): """DHT22 Temperature and Humidity Sensor.""" - def __init__(self, config: GpioConfig): + def __init__(self, config: GpioDriverConfig): super().__init__(config=config) self._dht_type = DHTType.DHT22 -IoFactory().register(ptype=__name__, config=DhtConfig, factory=DHT22) +DriverFactory().register(ptype=__name__, config=DhtConfig, factory=DHT22) diff --git a/lib/py_edge_device/carlos/edge/device/io/relay.py b/lib/py_edge_device/carlos/edge/device/driver/relay.py similarity index 71% rename from lib/py_edge_device/carlos/edge/device/io/relay.py rename to lib/py_edge_device/carlos/edge/device/driver/relay.py index 027d3897..416c2b49 100644 --- a/lib/py_edge_device/carlos/edge/device/io/relay.py +++ b/lib/py_edge_device/carlos/edge/device/driver/relay.py @@ -1,12 +1,12 @@ from typing import Literal -from carlos.edge.interface.device import DigitalOutput, GpioConfig, IoFactory +from carlos.edge.interface.device import DigitalOutput, DriverFactory, GpioDriverConfig from pydantic import Field from carlos.edge.device.protocol import GPIO -class RelayConfig(GpioConfig): +class RelayConfig(GpioDriverConfig): direction: Literal["output"] = Field("output") @@ -25,4 +25,4 @@ def set(self, value: bool): GPIO.output(self.config.pin, value) -IoFactory().register(ptype=__name__, config=RelayConfig, factory=Relay) +DriverFactory().register(ptype=__name__, config=RelayConfig, factory=Relay) diff --git a/lib/py_edge_device/carlos/edge/device/io/si1145.py b/lib/py_edge_device/carlos/edge/device/driver/si1145.py similarity index 97% rename from lib/py_edge_device/carlos/edge/device/io/si1145.py rename to lib/py_edge_device/carlos/edge/device/driver/si1145.py index a01b1674..574c45c6 100644 --- a/lib/py_edge_device/carlos/edge/device/io/si1145.py +++ b/lib/py_edge_device/carlos/edge/device/driver/si1145.py @@ -1,13 +1,13 @@ import time from typing import Literal -from carlos.edge.interface.device import AnalogInput, I2cConfig, IoFactory +from carlos.edge.interface.device import AnalogInput, DriverFactory, I2cDriverConfig from pydantic import Field from carlos.edge.device.protocol import I2C -class Si1145Config(I2cConfig): +class Si1145Config(I2cDriverConfig): direction: Literal["input"] = Field("input") @@ -51,7 +51,7 @@ def read(self) -> dict[str, float]: } -IoFactory().register(ptype=__name__, config=Si1145Config, factory=SI1145) +DriverFactory().register(ptype=__name__, config=Si1145Config, factory=SI1145) class SDL_Pi_SI1145: @@ -469,16 +469,16 @@ def _convert_raw_to_lux( # Get gain gain = 1 - # These are set to defaults in the Adafruit driver - - # need to change if you change them in the SI1145 driver + # These are set to defaults in the Adafruit driver_module - + # need to change if you change them in the SI1145 driver_module # range_ = self.read_param(parameter=adc_misc) # if (range_ & 32) == 32: # gain = 14.5 # Get sensitivity multiplier = 1 - # These are set to defaults in the Adafruit driver - - # need to change if you change them in the SI1145 driver + # These are set to defaults in the Adafruit driver_module - + # need to change if you change them in the SI1145 driver_module # sensitivity = self.read_param(parameter=adc_gain) # if (sensitivity & 7) == 0: # multiplier = 1 diff --git a/lib/py_edge_device/carlos/edge/device/runtime.py b/lib/py_edge_device/carlos/edge/device/runtime.py index 64487b3b..e3a03292 100644 --- a/lib/py_edge_device/carlos/edge/device/runtime.py +++ b/lib/py_edge_device/carlos/edge/device/runtime.py @@ -8,7 +8,7 @@ from apscheduler import AsyncScheduler from apscheduler.triggers.interval import IntervalTrigger from carlos.edge.interface import DeviceId, EdgeConnectionDisconnected, EdgeProtocol -from carlos.edge.interface.device.io import validate_device_address_space +from carlos.edge.interface.device.driver import validate_device_address_space from carlos.edge.interface.protocol import PING from loguru import logger diff --git a/lib/py_edge_device/pyproject.toml b/lib/py_edge_device/pyproject.toml index 25a77e36..77bcc5d3 100644 --- a/lib/py_edge_device/pyproject.toml +++ b/lib/py_edge_device/pyproject.toml @@ -66,5 +66,5 @@ omit = [ # omit all tests "*_test.py", # depends on actual device and hardware - "carlos/edge/device/io/*.py" + "carlos/edge/device/driver/*.py" ] diff --git a/lib/py_edge_device/tests/test_data/device_config b/lib/py_edge_device/tests/test_data/device_config index 75a7d0b4..4af27a32 100644 --- a/lib/py_edge_device/tests/test_data/device_config +++ b/lib/py_edge_device/tests/test_data/device_config @@ -1,30 +1,30 @@ io: - identifier: temp-humi-intern - driver: dht11 + driver_module: dht11 pin: 4 - identifier: uv-light - driver: si1145 + driver_module: si1145 - identifier: pump - driver: relay + driver_module: relay pin: 20 - identifier: valve-1 - driver: relay + driver_module: relay pin: 21 - identifier: valve-2 - driver: relay + driver_module: relay pin: 22 - identifier: valve-3 - driver: relay + driver_module: relay pin: 23 - identifier: valve-4 - driver: carlos.edge.device.io.relay + driver_module: carlos.edge.device.driver.relay pin: 24 - identifier: relay-6 - driver: carlos.edge.device.io.relay + driver_module: carlos.edge.device.driver.relay pin: 25 - identifier: relay-7 - driver: carlos.edge.device.io.relay + driver_module: carlos.edge.device.driver.relay pin: 26 - identifier: relay-8 - driver: carlos.edge.device.io.relay + driver_module: carlos.edge.device.driver.relay pin: 27 diff --git a/lib/py_edge_interface/carlos/edge/interface/device/__init__.py b/lib/py_edge_interface/carlos/edge/interface/device/__init__.py index cd4a4635..6ca4894c 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/__init__.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/__init__.py @@ -1,12 +1,12 @@ __all__ = [ "AnalogInput", - "CarlosIO", + "CarlosDriver", "DigitalOutput", - "GpioConfig", - "I2cConfig", - "IoConfig", - "IoFactory", + "DriverConfig", + "DriverFactory", + "GpioDriverConfig", + "I2cDriverConfig", ] -from .config import GpioConfig, I2cConfig, IoConfig -from .io import AnalogInput, CarlosIO, DigitalOutput, IoFactory +from .driver import AnalogInput, CarlosDriver, DigitalOutput, DriverFactory +from .driver_config import DriverConfig, GpioDriverConfig, I2cDriverConfig diff --git a/lib/py_edge_interface/carlos/edge/interface/device/io.py b/lib/py_edge_interface/carlos/edge/interface/device/driver.py similarity index 71% rename from lib/py_edge_interface/carlos/edge/interface/device/io.py rename to lib/py_edge_interface/carlos/edge/interface/device/driver.py index 6eff9cd8..949252fd 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/io.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver.py @@ -1,8 +1,8 @@ __all__ = [ "AnalogInput", "DigitalOutput", - "CarlosIO", - "IoFactory", + "CarlosDriver", + "DriverFactory", "validate_device_address_space", ] import asyncio @@ -12,19 +12,19 @@ from time import sleep from typing import Any, Callable, Generic, Iterable, TypeVar -from .config import GpioConfig, I2cConfig, IoConfig +from .driver_config import DriverConfig, GpioDriverConfig, I2cDriverConfig -IoConfigTypeVar = TypeVar("IoConfigTypeVar", bound=IoConfig) +DriverConfigTypeVar = TypeVar("DriverConfigTypeVar", bound=DriverConfig) -class CarlosPeripheral(ABC, Generic[IoConfigTypeVar]): - """Common base class for all peripherals.""" +class CarlosDriverBase(ABC, Generic[DriverConfigTypeVar]): + """Common base class for all drivers.""" - def __init__(self, config: IoConfigTypeVar): - self.config: IoConfigTypeVar = config + def __init__(self, config: DriverConfigTypeVar): + self.config: DriverConfigTypeVar = config def __str__(self): - return f"{self.config.identifier} ({self.config.driver})" + return f"{self.config.identifier} ({self.config.driver_module})" @property def identifier(self): @@ -42,7 +42,7 @@ def test(self): pass -class AnalogInput(CarlosPeripheral, ABC): +class AnalogInput(CarlosDriverBase, ABC): """Common base class for all analog input peripherals.""" @abstractmethod @@ -66,7 +66,7 @@ async def read_async(self) -> dict[str, float]: return await loop.run_in_executor(executor=pool, func=self.read) -class DigitalOutput(CarlosPeripheral, ABC): +class DigitalOutput(CarlosDriverBase, ABC): """Common base class for all digital output peripherals.""" @abstractmethod @@ -83,29 +83,29 @@ def test(self): self.set(False) -CarlosIO = AnalogInput | DigitalOutput +CarlosDriver = AnalogInput | DigitalOutput -FactoryItem = namedtuple("FactoryItem", ["config", "factory"]) +DriverDefinition = namedtuple("DriverDefinition", ["config", "factory"]) -class IoFactory: +class DriverFactory: """A singleton factory for io peripherals.""" _instance = None - _driver_to_io_type: dict[str, FactoryItem] = {} + _driver_index: dict[str, DriverDefinition] = {} def __new__(cls): if cls._instance is None: - cls._instance = super(IoFactory, cls).__new__(cls) - cls._instance._driver_to_io_type = {} + cls._instance = super(DriverFactory, cls).__new__(cls) + cls._instance._driver_index = {} return cls._instance def register( self, ptype: str, - config: type[IoConfigTypeVar], - factory: Callable[[IoConfigTypeVar], CarlosIO], + config: type[DriverConfigTypeVar], + factory: Callable[[DriverConfigTypeVar], CarlosDriver], ): """Registers a peripheral with the peripheral registry. @@ -114,45 +114,47 @@ def register( :param factory: The peripheral factory function. """ - if not issubclass(config, IoConfig): + if not issubclass(config, DriverConfig): raise ValueError( - "The config must be a subclass of IoConfig. " - "Please ensure that the config class is a subclass of IoConfig." + "The config must be a subclass of DriverConfig. " + "Please ensure that the config class is a subclass of DriverConfig." ) - if ptype in self._driver_to_io_type: + if ptype in self._driver_index: raise RuntimeError(f"The peripheral {ptype} is already registered.") - self._driver_to_io_type[ptype] = FactoryItem(config, factory) + self._driver_index[ptype] = DriverDefinition(config, factory) - def build(self, config: dict[str, Any]) -> CarlosIO: + def build(self, config: dict[str, Any]) -> CarlosDriver: """Builds a IO object from its configuration. :param config: The raw configuration. The schema must adhere to the - IoConfig model. But we require the full config as the ios may require + DriverConfig model. But we require the full config as the ios may require additional parameters. :returns: The IO object. """ - io_config = IoConfig.model_validate(config) + io_config = DriverConfig.model_validate(config) - if io_config.driver not in self._driver_to_io_type: + if io_config.driver_module not in self._driver_index: raise RuntimeError( - f"The driver {io_config.driver} is not registered." - f"Make sure to register `IoFactory().register(...)` " + f"The driver {io_config.driver_module} is not registered." + f"Make sure to register `DriverFactory().register(...)` " f"the peripheral before building it." ) - entry = self._driver_to_io_type[io_config.driver] + driver_definition = self._driver_index[io_config.driver_module] - return entry.factory(entry.config.model_validate(config)) + return driver_definition.factory( + driver_definition.config.model_validate(config) + ) I2C_PINS = [2, 3] """The Pin numbers designated for I2C communication.""" -def validate_device_address_space(ios: Iterable[CarlosIO]): +def validate_device_address_space(ios: Iterable[CarlosDriver]): """This function ensures that the configured pins and addresses are unique. :param ios: The list of IOs to validate. @@ -163,8 +165,8 @@ def validate_device_address_space(ios: Iterable[CarlosIO]): configs = [io.config for io in ios] - gpio_configs: list[GpioConfig] = [ - io for io in configs if isinstance(io, GpioConfig) + gpio_configs: list[GpioDriverConfig] = [ + io for io in configs if isinstance(io, GpioDriverConfig) ] # Ensure GPIO pins are unique @@ -180,7 +182,9 @@ def validate_device_address_space(ios: Iterable[CarlosIO]): f"Please ensure that each GPIO pin is configured only once." ) - i2c_configs: list[I2cConfig] = [io for io in configs if isinstance(io, I2cConfig)] + i2c_configs: list[I2cDriverConfig] = [ + io for io in configs if isinstance(io, I2cDriverConfig) + ] if i2c_configs: if any(gpio.pin in I2C_PINS for gpio in gpio_configs): raise ValueError( diff --git a/lib/py_edge_interface/carlos/edge/interface/device/config.py b/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py similarity index 79% rename from lib/py_edge_interface/carlos/edge/interface/device/config.py rename to lib/py_edge_interface/carlos/edge/interface/device/driver_config.py index 880dc3e1..183f730f 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/config.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py @@ -1,4 +1,8 @@ -__all__ = ["GpioConfig", "I2cConfig", "IoConfig"] +__all__ = [ + "DriverConfig", + "GpioDriverConfig", + "I2cDriverConfig", +] import importlib from typing import Literal @@ -10,25 +14,25 @@ # 3.3V, 2, 3, 4, GND, 17, 27, 22, 3.3V, 10, 9, 11, GND, ID EEPROM, 5, 6, 13, 19, 26, GND # Inner pins # noqa -class IoConfig(BaseModel): - """Common base class for all IO configurations.""" +class DriverConfig(BaseModel): + """Common base class for all driver_module configurations.""" identifier: str = Field( ..., - description="A unique identifier for the IO configuration. " + description="A unique identifier for the driver_module configuration. " "It is used to allow changing addresses, pins if required later.", ) - driver: str = Field( + driver_module: str = Field( ..., - description="Refers to the module name that implements the IO driver. " - "Built-in drivers located in carlos.edge.device.io module " - "don't need to specify the full path. Each driver module" - "must make a call to the IoFactory.register method to register" + description="Refers to the module name that implements the IO driver_module. " + "Built-in drivers located in carlos.edge.device.driver module " + "don't need to specify the full path. Each driver_module module" + "must make a call to the DriverFactory.register method to register" "itself.", ) - @field_validator("driver", mode="after") + @field_validator("driver_module", mode="after") def _validate_driver(cls, value): """Converts a module name to a full module path.""" @@ -36,7 +40,7 @@ def _validate_driver(cls, value): try: importlib.import_module(value) except ModuleNotFoundError: - abs_module = "carlos.edge.device.io" + "." + value + abs_module = "carlos.edge.device.driver" + "." + value try: importlib.import_module(abs_module) except ModuleNotFoundError: # pragma: no cover @@ -52,7 +56,7 @@ class DirectionMixin(BaseModel): ) -class GpioConfig(IoConfig, DirectionMixin): +class GpioDriverConfig(DriverConfig, DirectionMixin): """Defines a single input configuration.""" protocol: Literal["gpio"] = Field( @@ -90,7 +94,7 @@ class GpioConfig(IoConfig, DirectionMixin): ] = Field(..., description="The GPIO pin number.") -class I2cConfig(IoConfig, DirectionMixin): +class I2cDriverConfig(DriverConfig, DirectionMixin): """Defines a single input configuration.""" protocol: Literal["i2c"] = Field( From 3e8c4dd3918e56fb0dc2afbb2127d39985e8e506 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 17:47:05 +0200 Subject: [PATCH 43/57] fix lock files --- lib/py_edge_device/poetry.lock | 163 +++++++++++++++++++++++++++++++++ services/device/poetry.lock | 95 +++++++++++++++++++ 2 files changed, 258 insertions(+) diff --git a/lib/py_edge_device/poetry.lock b/lib/py_edge_device/poetry.lock index 7954ef1e..49b1fc7b 100644 --- a/lib/py_edge_device/poetry.lock +++ b/lib/py_edge_device/poetry.lock @@ -121,6 +121,31 @@ d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "bump-my-version" +version = "0.20.1" +description = "Version bump your Python project" +optional = false +python-versions = ">=3.8" +files = [ + {file = "bump-my-version-0.20.1.tar.gz", hash = "sha256:3ffea0cd72dd54517d8db596d31fd6415eb979bddd3f00d415f2cec1c055770d"}, + {file = "bump_my_version-0.20.1-py3-none-any.whl", hash = "sha256:d7d5dd61415fbda7e7b10d54a7186b80f81cb535da7fe9390ba0efabbe3d3a6f"}, +] + +[package.dependencies] +click = "*" +pydantic = ">=2.0.0" +pydantic-settings = "*" +questionary = "*" +rich = "*" +rich-click = "*" +tomlkit = "*" + +[package.extras] +dev = ["generate-changelog (>=0.7.6)", "git-fame (>=1.12.2)", "pip-tools", "pre-commit"] +docs = ["black", "markdown-customblocks", "mdx-truly-sane-lists", "mkdocs", "mkdocs-click", "mkdocs-drawio", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkdocs-git-committers-plugin", "mkdocs-git-revision-date-localized-plugin", "mkdocs-include-markdown-plugin", "mkdocs-literate-nav", "mkdocs-material", "mkdocstrings[python]", "python-frontmatter"] +test = ["coverage", "freezegun", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytest-sugar"] + [[package]] name = "carlos-edge-interface" version = "0.1.0" @@ -347,6 +372,7 @@ develop = false [package.dependencies] black = "^24.3.0" +bump-my-version = "^0.20.1" dirty-equals = "^0.7.0" docker = "^6.1.3" greenlet = "^3.0.3" @@ -528,6 +554,41 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} [package.extras] dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "mypy" version = "1.9.0" @@ -637,6 +698,20 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "prompt-toolkit" +version = "3.0.36" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.6.2" +files = [ + {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, + {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, +] + +[package.dependencies] +wcwidth = "*" + [[package]] name = "psutil" version = "5.9.8" @@ -794,6 +869,21 @@ python-dotenv = ">=0.21.0" toml = ["tomli (>=2.0.1)"] yaml = ["pyyaml (>=6.0.1)"] +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pytest" version = "8.1.1" @@ -947,6 +1037,20 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "questionary" +version = "2.0.1" +description = "Python library to build pretty command line user prompts ⭐️" +optional = false +python-versions = ">=3.8" +files = [ + {file = "questionary-2.0.1-py3-none-any.whl", hash = "sha256:8ab9a01d0b91b68444dff7f6652c1e754105533f083cbe27597c8110ecc230a2"}, + {file = "questionary-2.0.1.tar.gz", hash = "sha256:bcce898bf3dbb446ff62830c86c5c6fb9a22a54146f0f5597d3da43b10d8fc8b"}, +] + +[package.dependencies] +prompt_toolkit = ">=2.0,<=3.0.36" + [[package]] name = "requests" version = "2.31.0" @@ -968,6 +1072,43 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "rich" +version = "13.7.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "rich-click" +version = "1.7.4" +description = "Format click help output nicely with rich" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rich-click-1.7.4.tar.gz", hash = "sha256:7ce5de8e4dc0333aec946113529b3eeb349f2e5d2fafee96b9edf8ee36a01395"}, + {file = "rich_click-1.7.4-py3-none-any.whl", hash = "sha256:e363655475c60fec5a3e16a1eb618118ed79e666c365a36006b107c17c93ac4e"}, +] + +[package.dependencies] +click = ">=7" +rich = ">=10.7.0" +typing-extensions = "*" + +[package.extras] +dev = ["flake8", "flake8-docstrings", "mypy", "packaging", "pre-commit", "pytest", "pytest-cov", "types-setuptools"] + [[package]] name = "rpi-gpio" version = "0.7.1" @@ -1149,6 +1290,17 @@ files = [ [package.extras] doc = ["reno", "sphinx", "tornado (>=4.5)"] +[[package]] +name = "tomlkit" +version = "0.12.4" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, + {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, +] + [[package]] name = "types-psutil" version = "5.9.5.20240316" @@ -1227,6 +1379,17 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + [[package]] name = "websocket-client" version = "1.7.0" diff --git a/services/device/poetry.lock b/services/device/poetry.lock index ff3f1b7d..274ce7a3 100644 --- a/services/device/poetry.lock +++ b/services/device/poetry.lock @@ -121,6 +121,31 @@ d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "bump-my-version" +version = "0.20.1" +description = "Version bump your Python project" +optional = false +python-versions = ">=3.8" +files = [ + {file = "bump-my-version-0.20.1.tar.gz", hash = "sha256:3ffea0cd72dd54517d8db596d31fd6415eb979bddd3f00d415f2cec1c055770d"}, + {file = "bump_my_version-0.20.1-py3-none-any.whl", hash = "sha256:d7d5dd61415fbda7e7b10d54a7186b80f81cb535da7fe9390ba0efabbe3d3a6f"}, +] + +[package.dependencies] +click = "*" +pydantic = ">=2.0.0" +pydantic-settings = "*" +questionary = "*" +rich = "*" +rich-click = "*" +tomlkit = "*" + +[package.extras] +dev = ["generate-changelog (>=0.7.6)", "git-fame (>=1.12.2)", "pip-tools", "pre-commit"] +docs = ["black", "markdown-customblocks", "mdx-truly-sane-lists", "mkdocs", "mkdocs-click", "mkdocs-drawio", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkdocs-git-committers-plugin", "mkdocs-git-revision-date-localized-plugin", "mkdocs-include-markdown-plugin", "mkdocs-literate-nav", "mkdocs-material", "mkdocstrings[python]", "python-frontmatter"] +test = ["coverage", "freezegun", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytest-sugar"] + [[package]] name = "carlos-edge-device" version = "0.1.1" @@ -371,6 +396,7 @@ develop = false [package.dependencies] black = "^24.3.0" +bump-my-version = "^0.20.1" dirty-equals = "^0.7.0" docker = "^6.1.3" greenlet = "^3.0.3" @@ -752,6 +778,20 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "prompt-toolkit" +version = "3.0.36" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.6.2" +files = [ + {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, + {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, +] + +[package.dependencies] +wcwidth = "*" + [[package]] name = "psutil" version = "5.9.8" @@ -1077,6 +1117,20 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "questionary" +version = "2.0.1" +description = "Python library to build pretty command line user prompts ⭐️" +optional = false +python-versions = ">=3.8" +files = [ + {file = "questionary-2.0.1-py3-none-any.whl", hash = "sha256:8ab9a01d0b91b68444dff7f6652c1e754105533f083cbe27597c8110ecc230a2"}, + {file = "questionary-2.0.1.tar.gz", hash = "sha256:bcce898bf3dbb446ff62830c86c5c6fb9a22a54146f0f5597d3da43b10d8fc8b"}, +] + +[package.dependencies] +prompt_toolkit = ">=2.0,<=3.0.36" + [[package]] name = "requests" version = "2.31.0" @@ -1116,6 +1170,25 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "rich-click" +version = "1.7.4" +description = "Format click help output nicely with rich" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rich-click-1.7.4.tar.gz", hash = "sha256:7ce5de8e4dc0333aec946113529b3eeb349f2e5d2fafee96b9edf8ee36a01395"}, + {file = "rich_click-1.7.4-py3-none-any.whl", hash = "sha256:e363655475c60fec5a3e16a1eb618118ed79e666c365a36006b107c17c93ac4e"}, +] + +[package.dependencies] +click = ">=7" +rich = ">=10.7.0" +typing-extensions = "*" + +[package.extras] +dev = ["flake8", "flake8-docstrings", "mypy", "packaging", "pre-commit", "pytest", "pytest-cov", "types-setuptools"] + [[package]] name = "rpi-gpio" version = "0.7.1" @@ -1308,6 +1381,17 @@ files = [ [package.extras] doc = ["reno", "sphinx", "tornado (>=4.5)"] +[[package]] +name = "tomlkit" +version = "0.12.4" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, + {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, +] + [[package]] name = "typer" version = "0.12.3" @@ -1381,6 +1465,17 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + [[package]] name = "websocket-client" version = "1.7.0" From b3da6e1487e72274e6f8ed2b664715d82f181e66 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 17:49:49 +0200 Subject: [PATCH 44/57] further renaming --- lib/py_edge_device/carlos/edge/device/runtime.py | 4 ++-- services/device/device/cli/config.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/runtime.py b/lib/py_edge_device/carlos/edge/device/runtime.py index e3a03292..c273c87c 100644 --- a/lib/py_edge_device/carlos/edge/device/runtime.py +++ b/lib/py_edge_device/carlos/edge/device/runtime.py @@ -29,7 +29,7 @@ def __init__(self, device_id: DeviceId, protocol: EdgeProtocol): self.device_id = device_id self.protocol = protocol - self.io_manager = IoManager() + self.driver_manager = DriverManager() async def run(self): """Runs the device runtime.""" @@ -71,7 +71,7 @@ def _prepare_runtime(self): ) -class IoManager: # pragma: no cover +class DriverManager: # pragma: no cover def __init__(self): diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index a277f02e..ac28dcb6 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -3,7 +3,7 @@ from typing import TypeVar import typer -from carlos.edge.device.runtime import IoManager +from carlos.edge.device.runtime import DriverManager from pydantic import BaseModel from pydantic_core import PydanticUndefinedType from rich import print, print_json @@ -66,17 +66,17 @@ def test(): # pragma: no cover exceptions = {} results = {} - for io in IoManager().setup().ios: - console.print(f"[cyan]Testing {io} ... [/cyan]", end="") + for driver in DriverManager().setup().ios: + console.print(f"[cyan]Testing {driver} ... [/cyan]", end="") try: - result = io.test() + result = driver.test() console.print("[green]passed[/green]") if result: - results[io.identifier] = result + results[driver.identifier] = result except Exception as e: console.print("[red]failed[/red]") console.print_exception() - exceptions[io.identifier] = e + exceptions[driver.identifier] = e if results: console.print("\nThe following IO peripherals returned data:") From f9a628ceda7e045c594be5631c10cc525f3ee441 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 19 Apr 2024 15:57:25 +0000 Subject: [PATCH 45/57] =?UTF-8?q?Bump=20lib/py=5Fedge=5Finterface=20versio?= =?UTF-8?q?n:=200.1.0=20=E2=86=92=200.1.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/py_edge_interface/pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/py_edge_interface/pyproject.toml b/lib/py_edge_interface/pyproject.toml index f52f7c76..e3b3746f 100644 --- a/lib/py_edge_interface/pyproject.toml +++ b/lib/py_edge_interface/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "carlos.edge.interface" -version = "0.1.0" +version = "0.1.1" description = "Shared library to handle the edge communication." authors = ["Felix Fanghanel"] license = "MIT" @@ -23,7 +23,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.bumpversion] -current_version = "0.1.0" +current_version = "0.1.1" commit = true tag = false parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z0-9\\.]+))?" From a0a063f2f083a6beb087235556ef9d154a7ae37f Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 18:30:52 +0200 Subject: [PATCH 46/57] =?UTF-8?q?Bump=20version:=200.1.1=20=E2=86=92=200.1?= =?UTF-8?q?.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/py_edge_device/pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/py_edge_device/pyproject.toml b/lib/py_edge_device/pyproject.toml index b959d939..fa10b7ee 100644 --- a/lib/py_edge_device/pyproject.toml +++ b/lib/py_edge_device/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "carlos.edge.device" -version = "0.1.1" +version = "0.1.2" description = "The library for the edge device of the carlos project." authors = ["Felix Fanghanel"] license = "MIT" @@ -31,7 +31,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.bumpversion] -current_version = "0.1.1" +current_version = "0.1.2" commit = true tag = false parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\-(?P[a-z0-9\\.]+))?" From b956d3f920765c5716e46c8f488ab6672f0cdc08 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 18:55:05 +0200 Subject: [PATCH 47/57] config covered --- lib/py_edge_device/poetry.lock | 2 +- .../edge/interface/device/driver_config.py | 6 +- .../interface/device/driver_config_test.py | 82 ++++++ .../edge/interface/device/driver_test.py | 0 lib/py_edge_interface/poetry.lock | 264 +++++++++++++++++- lib/py_edge_interface/pyproject.toml | 1 + 6 files changed, 350 insertions(+), 5 deletions(-) create mode 100644 lib/py_edge_interface/carlos/edge/interface/device/driver_config_test.py create mode 100644 lib/py_edge_interface/carlos/edge/interface/device/driver_test.py diff --git a/lib/py_edge_device/poetry.lock b/lib/py_edge_device/poetry.lock index 49b1fc7b..569195c7 100644 --- a/lib/py_edge_device/poetry.lock +++ b/lib/py_edge_device/poetry.lock @@ -148,7 +148,7 @@ test = ["coverage", "freezegun", "pre-commit", "pytest", "pytest-cov", "pytest-m [[package]] name = "carlos-edge-interface" -version = "0.1.0" +version = "0.1.1" description = "Shared library to handle the edge communication." optional = false python-versions = ">=3.11,<3.12" diff --git a/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py b/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py index 183f730f..75c9b979 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py @@ -33,7 +33,7 @@ class DriverConfig(BaseModel): ) @field_validator("driver_module", mode="after") - def _validate_driver(cls, value): + def _validate_driver_module(cls, value): """Converts a module name to a full module path.""" # check if the given module exists in the current working directory. @@ -43,7 +43,7 @@ def _validate_driver(cls, value): abs_module = "carlos.edge.device.driver" + "." + value try: importlib.import_module(abs_module) - except ModuleNotFoundError: # pragma: no cover + except ModuleNotFoundError: raise ValueError(f"The module {value} ({abs_module}) does not exist.") value = abs_module @@ -122,7 +122,7 @@ def validate_address(cls, value): if not 0x03 <= int(value) <= 0x77: raise ValueError("The valid I2C address range is 0x03 to 0x77.") - return hex(value) + return f"0x{value:02x}" @property def address_int(self): diff --git a/lib/py_edge_interface/carlos/edge/interface/device/driver_config_test.py b/lib/py_edge_interface/carlos/edge/interface/device/driver_config_test.py new file mode 100644 index 00000000..194abad4 --- /dev/null +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver_config_test.py @@ -0,0 +1,82 @@ +from contextlib import nullcontext + +import pytest +from pydantic import ValidationError + +from .driver_config import DriverConfig, I2cDriverConfig + +VALID_DRIVER_MODULE = __package__ +"""For a driver module to be valid, it must be importable. Everything else +is checked else where.""" + + +class TestDriverConfig: + + @pytest.mark.parametrize( + "driver_module, expected", + [ + pytest.param(VALID_DRIVER_MODULE, VALID_DRIVER_MODULE, id="valid module"), + pytest.param( + "relay", + "carlos.edge.device.driver.relay", + id="built-in relative module", + ), + pytest.param("non_existing_module", ValueError, id="invalid module"), + ], + ) + def test_driver_module_validation( + self, driver_module: str, expected: str | type[Exception] + ): + """This function ensures that the driver module is valid.""" + + if isinstance(expected, str): + context = nullcontext() + else: + context = pytest.raises(expected) + + with context: + config = DriverConfig( + identifier="does-not-matter", driver_module=driver_module + ) + + assert config.driver_module == expected + + +class TestI2cDriverConfig: + + @pytest.mark.parametrize( + "address, expected", + [ + pytest.param("0x03", "0x03", id="string: minimum address"), + pytest.param("0x77", "0x77", id="string: maximum address"), + pytest.param("0x00", ValidationError, id="string: below minimum address"), + pytest.param("0x78", ValidationError, id="string: above maximum address"), + pytest.param("1e", "0x1e", id="string: valid address without 0x"), + pytest.param("0xij", ValidationError, id="string: invalid hex"), + pytest.param("0x0A", "0x0a", id="string: upper case hex"), + pytest.param(0x03, "0x03", id="int: minimum address"), + pytest.param(0x77, "0x77", id="int: maximum address"), + pytest.param(0x00, ValidationError, id="int: below minimum address"), + pytest.param(0x78, ValidationError, id="int: above maximum address"), + ], + ) + def test_address_validation( + self, address: str | int, expected: str | type[Exception] + ): + """This function ensures that the address is valid.""" + + if isinstance(expected, str): + context = nullcontext() + else: + context = pytest.raises(expected) + + with context: + config = I2cDriverConfig( + identifier="does-not-matter", + driver_module=VALID_DRIVER_MODULE, + direction="input", # does not matter for this test + address=address, + ) + + assert config.address == expected + assert config.address_int == int(config.address, 16) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py b/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py new file mode 100644 index 00000000..e69de29b diff --git a/lib/py_edge_interface/poetry.lock b/lib/py_edge_interface/poetry.lock index 442d7d57..ca0d3316 100644 --- a/lib/py_edge_interface/poetry.lock +++ b/lib/py_edge_interface/poetry.lock @@ -11,6 +11,72 @@ files = [ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, ] +[[package]] +name = "anyio" +version = "4.3.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + +[[package]] +name = "apscheduler" +version = "4.0.0a4" +description = "In-process task scheduler with Cron-like capabilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "APScheduler-4.0.0a4-py3-none-any.whl", hash = "sha256:29576119a82a4e91fbd20971d9225c88b073b8e23a7e6bda5f9d555fa8bcccfc"}, + {file = "APScheduler-4.0.0a4.tar.gz", hash = "sha256:c349f281ec505235e1a748b0954d1ab868acefab69815559d62e025686945e65"}, +] + +[package.dependencies] +anyio = ">=4.0,<5.0" +attrs = ">=21.3" +tenacity = ">=8.0,<9.0" +tzlocal = ">=3.0" + +[package.extras] +asyncpg = ["asyncpg (>=0.20)"] +cbor = ["cbor2 (>=5.0)"] +doc = ["sphinx", "sphinx-autodoc-typehints", "sphinx-rtd-theme (>=1.3.0)", "sphinx-tabs (>=3.3.1)"] +mongodb = ["pymongo (>=4)"] +mqtt = ["paho-mqtt (>=1.5)"] +redis = ["redis (>=4.4.0)"] +sqlalchemy = ["sqlalchemy (>=2.0.19)"] +test = ["APScheduler[cbor,mongodb,mqtt,redis,sqlalchemy]", "PySide6 (>=6.6)", "aiosqlite (>=0.19)", "anyio[trio]", "asyncmy (>=0.2.5)", "asyncpg (>=0.20)", "coverage (>=7)", "freezegun", "paho-mqtt (>=1.5)", "psycopg", "pymongo (>=4)", "pymysql[rsa]", "pytest (>=7.4.0)", "pytest-freezer", "pytest-lazy-fixture", "pytest-mock", "uwsgi"] + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + [[package]] name = "black" version = "24.4.0" @@ -80,6 +146,30 @@ dev = ["generate-changelog (>=0.7.6)", "git-fame (>=1.12.2)", "pip-tools", "pre- docs = ["black", "markdown-customblocks", "mdx-truly-sane-lists", "mkdocs", "mkdocs-click", "mkdocs-drawio", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkdocs-git-committers-plugin", "mkdocs-git-revision-date-localized-plugin", "mkdocs-include-markdown-plugin", "mkdocs-literate-nav", "mkdocs-material", "mkdocstrings[python]", "python-frontmatter"] test = ["coverage", "freezegun", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytest-sugar"] +[[package]] +name = "carlos-edge-device" +version = "0.1.2" +description = "The library for the edge device of the carlos project." +optional = false +python-versions = ">=3.11,<3.12" +files = [] +develop = false + +[package.dependencies] +apscheduler = "^4.0.0a4" +"carlos.edge.interface" = {path = "../py_edge_interface"} +loguru = "^0.7.2" +psutil = "^5.9.8" +pydantic = "^2.6.4" +pyyaml = "^6.0.1" +rpi-gpio = {version = "^0.7.1", markers = "platform_machine == \"armv7l\" or platform_machine == \"aarch64\""} +semver = "^3.0.2" +smbus2 = "^0.4.3" + +[package.source] +type = "directory" +url = "../py_edge_device" + [[package]] name = "certifi" version = "2024.2.2" @@ -630,6 +720,34 @@ files = [ [package.dependencies] wcwidth = "*" +[[package]] +name = "psutil" +version = "5.9.8" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, + {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, + {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, + {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, + {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, + {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, + {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, + {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, + {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, + {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + [[package]] name = "pydantic" version = "2.7.0" @@ -878,6 +996,55 @@ files = [ {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, ] +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + [[package]] name = "questionary" version = "2.0.1" @@ -950,6 +1117,21 @@ typing-extensions = "*" [package.extras] dev = ["flake8", "flake8-docstrings", "mypy", "packaging", "pre-commit", "pytest", "pytest-cov", "types-setuptools"] +[[package]] +name = "rpi-gpio" +version = "0.7.1" +description = "A module to control Raspberry Pi GPIO channels" +optional = false +python-versions = "*" +files = [ + {file = "RPi.GPIO-0.7.1-cp27-cp27mu-linux_armv6l.whl", hash = "sha256:b86b66dc02faa5461b443a1e1f0c1d209d64ab5229696f32fb3b0215e0600c8c"}, + {file = "RPi.GPIO-0.7.1-cp310-cp310-linux_armv6l.whl", hash = "sha256:57b6c044ef5375a78c8dda27cdfadf329e76aa6943cd6cffbbbd345a9adf9ca5"}, + {file = "RPi.GPIO-0.7.1-cp37-cp37m-linux_armv6l.whl", hash = "sha256:77afb817b81331ce3049a4b8f94a85e41b7c404d8e56b61ac0f1eb75c3120868"}, + {file = "RPi.GPIO-0.7.1-cp38-cp38-linux_armv6l.whl", hash = "sha256:29226823da8b5ccb9001d795a944f2e00924eeae583490f0bc7317581172c624"}, + {file = "RPi.GPIO-0.7.1-cp39-cp39-linux_armv6l.whl", hash = "sha256:15311d3b063b71dee738cd26570effc9985a952454d162937c34e08c0fc99902"}, + {file = "RPi.GPIO-0.7.1.tar.gz", hash = "sha256:cd61c4b03c37b62bba4a5acfea9862749c33c618e0295e7e90aa4713fb373b70"}, +] + [[package]] name = "ruff" version = "0.3.7" @@ -976,6 +1158,44 @@ files = [ {file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"}, ] +[[package]] +name = "semver" +version = "3.0.2" +description = "Python helper for Semantic Versioning (https://semver.org)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4"}, + {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, +] + +[[package]] +name = "smbus2" +version = "0.4.3" +description = "smbus2 is a drop-in replacement for smbus-cffi/smbus-python in pure Python" +optional = false +python-versions = "*" +files = [ + {file = "smbus2-0.4.3-py2.py3-none-any.whl", hash = "sha256:a2fc29cfda4081ead2ed61ef2c4fc041d71dd40a8d917e85216f44786fca2d1d"}, + {file = "smbus2-0.4.3.tar.gz", hash = "sha256:36f2288a8e1a363cb7a7b2244ec98d880eb5a728a2494ac9c71e9de7bf6a803a"}, +] + +[package.extras] +docs = ["sphinx (>=1.5.3)"] +qa = ["flake8"] +test = ["mock", "nose"] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + [[package]] name = "sqlalchemy" version = "2.0.29" @@ -1064,6 +1284,20 @@ postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] sqlcipher = ["sqlcipher3_binary"] +[[package]] +name = "tenacity" +version = "8.2.3" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"}, + {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"}, +] + +[package.extras] +doc = ["reno", "sphinx", "tornado (>=4.5)"] + [[package]] name = "tomlkit" version = "0.12.4" @@ -1086,6 +1320,34 @@ files = [ {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] +[[package]] +name = "tzdata" +version = "2024.1" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + +[[package]] +name = "tzlocal" +version = "5.2" +description = "tzinfo object for the local timezone" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, + {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + [[package]] name = "urllib3" version = "2.2.1" @@ -1147,4 +1409,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.12" -content-hash = "035adfa2f1c228e2aa1194533693e7b874ffc5f9f486782662cc7cb4299679d9" +content-hash = "d3719212f8bab03aa3b13302be4af961e73bff6d58b5d6468b08c11517951523" diff --git a/lib/py_edge_interface/pyproject.toml b/lib/py_edge_interface/pyproject.toml index e3b3746f..3cb3393c 100644 --- a/lib/py_edge_interface/pyproject.toml +++ b/lib/py_edge_interface/pyproject.toml @@ -14,6 +14,7 @@ pydantic = "^2.6.4" [tool.poetry.group.dev.dependencies] "devtools" = {path = "../py_dev_dependencies"} +"carlos.edge.device" = {path = "../../lib/py_edge_device"} [tool.poetry.plugins.pytest11] qmulus_core = "carlos.edge.interface.plugin_pytest" From cd22b4611526e676bf67b09eafe35539fe758930 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 19:14:48 +0200 Subject: [PATCH 48/57] digital output test --- .../carlos/edge/interface/device/driver.py | 31 ++++++- .../edge/interface/device/driver_config.py | 1 + .../edge/interface/device/driver_test.py | 88 +++++++++++++++++++ 3 files changed, 117 insertions(+), 3 deletions(-) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/driver.py b/lib/py_edge_interface/carlos/edge/interface/device/driver.py index 949252fd..93e22c1c 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/driver.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver.py @@ -10,9 +10,14 @@ from abc import ABC, abstractmethod from collections import namedtuple from time import sleep -from typing import Any, Callable, Generic, Iterable, TypeVar +from typing import Any, Callable, Generic, Iterable, Self, TypeVar -from .driver_config import DriverConfig, GpioDriverConfig, I2cDriverConfig +from .driver_config import ( + DirectionMixin, + DriverConfig, + GpioDriverConfig, + I2cDriverConfig, +) DriverConfigTypeVar = TypeVar("DriverConfigTypeVar", bound=DriverConfig) @@ -31,7 +36,7 @@ def identifier(self): return self.config.identifier @abstractmethod - def setup(self): + def setup(self) -> Self: """Sets up the peripheral. This is required for testing. As the test runner is not able to run the setup method of the peripheral outside the device.""" pass @@ -45,6 +50,16 @@ def test(self): class AnalogInput(CarlosDriverBase, ABC): """Common base class for all analog input peripherals.""" + def __init__(self, config: DriverConfigTypeVar): + + if isinstance(config, DirectionMixin): + if config.direction != "input": + raise ValueError( + "Recieved a non-input configuration for an analog input." + ) + + super().__init__(config) + @abstractmethod def read(self) -> dict[str, float]: """Reads the value of the analog input. The return value is a dictionary @@ -69,6 +84,16 @@ async def read_async(self) -> dict[str, float]: class DigitalOutput(CarlosDriverBase, ABC): """Common base class for all digital output peripherals.""" + def __init__(self, config: DriverConfigTypeVar): + + if isinstance(config, DirectionMixin): + if config.direction != "output": + raise ValueError( + "Recieved a non-output configuration for a digital output." + ) + + super().__init__(config) + @abstractmethod def set(self, value: bool): pass diff --git a/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py b/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py index 75c9b979..d5a9f803 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py @@ -1,4 +1,5 @@ __all__ = [ + "DirectionMixin", "DriverConfig", "GpioDriverConfig", "I2cDriverConfig", diff --git a/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py b/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py index e69de29b..b7a31366 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py @@ -0,0 +1,88 @@ +from typing import Self + +import pytest + +from .driver import AnalogInput, DigitalOutput +from .driver_config import GpioDriverConfig + +ANALOG_INPUT_VALUE = {"value": 0.0} +ANALOG_INPUT_CONFIG = GpioDriverConfig( + identifier="analog-input-test", driver_module=__package__, direction="input", pin=13 +) +DIGITAL_OUTPUT_CONFIG = GpioDriverConfig( + identifier="digital-output-test", + driver_module=__package__, + direction="output", + pin=13, +) + + +class AnalogInputTest(AnalogInput): + + def setup(self): + pass + + def read(self) -> dict[str, float]: + return ANALOG_INPUT_VALUE + + +def test_carlos_driver_base(): + """This test test the methods of the CarlosDriverBase class via the + AnalogInputTest class.""" + + driver = AnalogInputTest(config=ANALOG_INPUT_CONFIG) + + assert isinstance(str(driver), str), "Could not convert driver to string." + assert ( + driver.identifier == ANALOG_INPUT_CONFIG.identifier + ), "Identifier should be the same as in the config." + + +def test_analog_input(): + """This test tests the AnalogInput Interface bia the AnalogInputTest class.""" + analog_input = AnalogInputTest(config=ANALOG_INPUT_CONFIG) + + assert ( + analog_input.test() == ANALOG_INPUT_VALUE + ), "Test function should return a reading." + + # using output config for input should raise an error + with pytest.raises(ValueError): + AnalogInputTest(config=DIGITAL_OUTPUT_CONFIG) + + +@pytest.mark.asyncio +async def test_async_analog_input(): + """This test tests the AnalogInput Interface bia the AnalogInputTest class.""" + analog_input = AnalogInputTest(config=ANALOG_INPUT_CONFIG) + + assert ( + await analog_input.read_async() == ANALOG_INPUT_VALUE + ), "Test function should return a reading." + + +class DigitalOutputTest(DigitalOutput): + + def setup(self) -> Self: + self.pytest_state = None + return self + + def set(self, value: bool): + self.pytest_state = value + + +def test_digital_output(): + """This test tests the DigitalOutput Interface bia the DigitalOutputTest class.""" + digital_output = DigitalOutputTest(config=DIGITAL_OUTPUT_CONFIG).setup() + + assert digital_output.pytest_state is None, "Initial state should be None." + + digital_output.test() + + assert ( + digital_output.pytest_state is not None + ), "State should be set to a value after running the test." + + # using input config for output should raise an error + with pytest.raises(ValueError): + DigitalOutputTest(config=ANALOG_INPUT_CONFIG) From fb51434d10ed14553ae9871f1d5f9c92b3129db2 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 19:26:32 +0200 Subject: [PATCH 49/57] driver factory tests --- .../edge/device/driver/device_metrics.py | 3 +- .../carlos/edge/device/driver/dht11.py | 2 +- .../carlos/edge/device/driver/dht22.py | 2 +- .../carlos/edge/device/driver/relay.py | 2 +- .../carlos/edge/device/driver/si1145.py | 2 +- .../carlos/edge/interface/device/driver.py | 13 +++-- .../interface/device/driver_config_test.py | 2 +- .../edge/interface/device/driver_test.py | 53 +++++++++++++++++-- 8 files changed, 65 insertions(+), 14 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/driver/device_metrics.py b/lib/py_edge_device/carlos/edge/device/driver/device_metrics.py index 9efa7011..ae844a20 100644 --- a/lib/py_edge_device/carlos/edge/device/driver/device_metrics.py +++ b/lib/py_edge_device/carlos/edge/device/driver/device_metrics.py @@ -32,4 +32,5 @@ def _read_cpu_temp() -> float: return 0.0 -DriverFactory().register(ptype=__name__, config=DriverConfig, factory=DeviceMetrics) +DriverFactory().register(driver_module=__name__, config=DriverConfig, + factory=DeviceMetrics) diff --git a/lib/py_edge_device/carlos/edge/device/driver/dht11.py b/lib/py_edge_device/carlos/edge/device/driver/dht11.py index d6c7f9ba..d91d10ac 100644 --- a/lib/py_edge_device/carlos/edge/device/driver/dht11.py +++ b/lib/py_edge_device/carlos/edge/device/driver/dht11.py @@ -13,4 +13,4 @@ def __init__(self, config: GpioDriverConfig): self._dht_type = DHTType.DHT11 -DriverFactory().register(ptype=__name__, config=DhtConfig, factory=DHT11) +DriverFactory().register(driver_module=__name__, config=DhtConfig, factory=DHT11) diff --git a/lib/py_edge_device/carlos/edge/device/driver/dht22.py b/lib/py_edge_device/carlos/edge/device/driver/dht22.py index e16b3e4d..f90c7a1a 100644 --- a/lib/py_edge_device/carlos/edge/device/driver/dht22.py +++ b/lib/py_edge_device/carlos/edge/device/driver/dht22.py @@ -13,4 +13,4 @@ def __init__(self, config: GpioDriverConfig): self._dht_type = DHTType.DHT22 -DriverFactory().register(ptype=__name__, config=DhtConfig, factory=DHT22) +DriverFactory().register(driver_module=__name__, config=DhtConfig, factory=DHT22) diff --git a/lib/py_edge_device/carlos/edge/device/driver/relay.py b/lib/py_edge_device/carlos/edge/device/driver/relay.py index 416c2b49..a372ebd1 100644 --- a/lib/py_edge_device/carlos/edge/device/driver/relay.py +++ b/lib/py_edge_device/carlos/edge/device/driver/relay.py @@ -25,4 +25,4 @@ def set(self, value: bool): GPIO.output(self.config.pin, value) -DriverFactory().register(ptype=__name__, config=RelayConfig, factory=Relay) +DriverFactory().register(driver_module=__name__, config=RelayConfig, factory=Relay) diff --git a/lib/py_edge_device/carlos/edge/device/driver/si1145.py b/lib/py_edge_device/carlos/edge/device/driver/si1145.py index 574c45c6..ee4c00e9 100644 --- a/lib/py_edge_device/carlos/edge/device/driver/si1145.py +++ b/lib/py_edge_device/carlos/edge/device/driver/si1145.py @@ -51,7 +51,7 @@ def read(self) -> dict[str, float]: } -DriverFactory().register(ptype=__name__, config=Si1145Config, factory=SI1145) +DriverFactory().register(driver_module=__name__, config=Si1145Config, factory=SI1145) class SDL_Pi_SI1145: diff --git a/lib/py_edge_interface/carlos/edge/interface/device/driver.py b/lib/py_edge_interface/carlos/edge/interface/device/driver.py index 93e22c1c..13400c36 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/driver.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver.py @@ -128,15 +128,17 @@ def __new__(cls): def register( self, - ptype: str, + driver_module: str, config: type[DriverConfigTypeVar], factory: Callable[[DriverConfigTypeVar], CarlosDriver], ): """Registers a peripheral with the peripheral registry. - :param ptype: The peripheral type. + :param driver_module: The peripheral type. :param config: The peripheral configuration model. :param factory: The peripheral factory function. + :raises ValueError: If the config is not a subclass of DriverConfig. + :raises RuntimeError: If the peripheral is already registered. """ if not issubclass(config, DriverConfig): @@ -145,10 +147,10 @@ def register( "Please ensure that the config class is a subclass of DriverConfig." ) - if ptype in self._driver_index: - raise RuntimeError(f"The peripheral {ptype} is already registered.") + if driver_module in self._driver_index: + raise RuntimeError(f"The peripheral {driver_module} is already registered.") - self._driver_index[ptype] = DriverDefinition(config, factory) + self._driver_index[driver_module] = DriverDefinition(config, factory) def build(self, config: dict[str, Any]) -> CarlosDriver: """Builds a IO object from its configuration. @@ -157,6 +159,7 @@ def build(self, config: dict[str, Any]) -> CarlosDriver: DriverConfig model. But we require the full config as the ios may require additional parameters. :returns: The IO object. + :raises RuntimeError: If the driver is not registered. """ io_config = DriverConfig.model_validate(config) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/driver_config_test.py b/lib/py_edge_interface/carlos/edge/interface/device/driver_config_test.py index 194abad4..b4b3280c 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/driver_config_test.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver_config_test.py @@ -5,7 +5,7 @@ from .driver_config import DriverConfig, I2cDriverConfig -VALID_DRIVER_MODULE = __package__ +VALID_DRIVER_MODULE = __name__ """For a driver module to be valid, it must be importable. Everything else is checked else where.""" diff --git a/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py b/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py index b7a31366..d52d0bdc 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py @@ -1,17 +1,23 @@ from typing import Self import pytest +from pydantic import BaseModel -from .driver import AnalogInput, DigitalOutput +from .driver import AnalogInput, DigitalOutput, DriverFactory from .driver_config import GpioDriverConfig +DRIVER_MODULE = __name__ + ANALOG_INPUT_VALUE = {"value": 0.0} ANALOG_INPUT_CONFIG = GpioDriverConfig( - identifier="analog-input-test", driver_module=__package__, direction="input", pin=13 + identifier="analog-input-test", + driver_module=DRIVER_MODULE, + direction="input", + pin=13, ) DIGITAL_OUTPUT_CONFIG = GpioDriverConfig( identifier="digital-output-test", - driver_module=__package__, + driver_module=DRIVER_MODULE, direction="output", pin=13, ) @@ -86,3 +92,44 @@ def test_digital_output(): # using input config for output should raise an error with pytest.raises(ValueError): DigitalOutputTest(config=ANALOG_INPUT_CONFIG) + + +def test_driver_factory(): + """This test tests the driver factory function.""" + + factory = DriverFactory() + + raw_analog_input_config = ANALOG_INPUT_CONFIG.model_dump(mode="json") + + # Sofar the AnalogInputTest class was never registered, thus the build method + # should raise a RuntimeError. + with pytest.raises(RuntimeError): + factory.build(raw_analog_input_config) + + factory.register( + driver_module=DRIVER_MODULE, config=GpioDriverConfig, factory=AnalogInputTest + ) + + # Trying to register a driver with the same driver_module should raise a RuntimeError. + with pytest.raises(RuntimeError): + factory.register( + driver_module=DRIVER_MODULE, + config=GpioDriverConfig, + factory=AnalogInputTest, + ) + + # Passing a non DriverConfig type as config should raise a ValueError. + with pytest.raises(ValueError): + factory.register( + driver_module=DRIVER_MODULE + ".test", + config=BaseModel, + factory=DigitalOutputTest, + ) + + driver = factory.build(raw_analog_input_config) + assert isinstance( + driver, AnalogInputTest + ), "Driver should be an instance of AnalogInputTest." + assert isinstance( + driver.config, GpioDriverConfig + ), "Config should be an instance of GpioDriverConfig." From 910c68923330236640133d0cb78dc2448d3c7106 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 19 Apr 2024 17:27:21 +0000 Subject: [PATCH 50/57] update lib/py_edge_device --- .../carlos/edge/device/driver/device_metrics.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/driver/device_metrics.py b/lib/py_edge_device/carlos/edge/device/driver/device_metrics.py index ae844a20..5d8b2a06 100644 --- a/lib/py_edge_device/carlos/edge/device/driver/device_metrics.py +++ b/lib/py_edge_device/carlos/edge/device/driver/device_metrics.py @@ -32,5 +32,6 @@ def _read_cpu_temp() -> float: return 0.0 -DriverFactory().register(driver_module=__name__, config=DriverConfig, - factory=DeviceMetrics) +DriverFactory().register( + driver_module=__name__, config=DriverConfig, factory=DeviceMetrics +) From 35fc7c80eb8fa53b92004275a61258ab25232418 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 19:37:36 +0200 Subject: [PATCH 51/57] boost coverage --- .../edge/device/driver/device_metrics.py | 5 +- .../carlos/edge/device/runtime.py | 6 +- .../carlos/edge/interface/device/driver.py | 32 ++++---- .../edge/interface/device/driver_test.py | 74 ++++++++++++++++++- services/device/device/cli/config.py | 2 +- 5 files changed, 95 insertions(+), 24 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/driver/device_metrics.py b/lib/py_edge_device/carlos/edge/device/driver/device_metrics.py index ae844a20..5d8b2a06 100644 --- a/lib/py_edge_device/carlos/edge/device/driver/device_metrics.py +++ b/lib/py_edge_device/carlos/edge/device/driver/device_metrics.py @@ -32,5 +32,6 @@ def _read_cpu_temp() -> float: return 0.0 -DriverFactory().register(driver_module=__name__, config=DriverConfig, - factory=DeviceMetrics) +DriverFactory().register( + driver_module=__name__, config=DriverConfig, factory=DeviceMetrics +) diff --git a/lib/py_edge_device/carlos/edge/device/runtime.py b/lib/py_edge_device/carlos/edge/device/runtime.py index c273c87c..5a6ff404 100644 --- a/lib/py_edge_device/carlos/edge/device/runtime.py +++ b/lib/py_edge_device/carlos/edge/device/runtime.py @@ -75,12 +75,12 @@ class DriverManager: # pragma: no cover def __init__(self): - self.ios = load_io() - validate_device_address_space(self.ios) + self.drivers = load_io() + validate_device_address_space(self.drivers) def setup(self) -> Self: """Sets up the I/O peripherals.""" - for io in self.ios: + for io in self.drivers: logger.debug(f"Setting up I/O peripheral {io}.") io.setup() diff --git a/lib/py_edge_interface/carlos/edge/interface/device/driver.py b/lib/py_edge_interface/carlos/edge/interface/device/driver.py index 13400c36..60520b7f 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/driver.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver.py @@ -182,16 +182,29 @@ def build(self, config: dict[str, Any]) -> CarlosDriver: """The Pin numbers designated for I2C communication.""" -def validate_device_address_space(ios: Iterable[CarlosDriver]): +def validate_device_address_space(drivers: Iterable[CarlosDriver]): """This function ensures that the configured pins and addresses are unique. - :param ios: The list of IOs to validate. + :param drivers: The list of IOs to validate. :raises ValueError: If any of the pins or addresses are configured more than once. If the GPIO pins 2 and 3 are when I2C communication is configured. If any of the identifiers are configured more than once. """ - configs = [io.config for io in ios] + # Ensure all identifiers are unique + seen_identifiers = set() + duplicate_identifiers = [ + driver.identifier + for driver in drivers + if driver.identifier in seen_identifiers or seen_identifiers.add(driver.identifier) # type: ignore[func-returns-value] # noqa: E501 + ] + if duplicate_identifiers: + raise ValueError( + f"The identifiers {duplicate_identifiers} are configured more than " + f"once. Please ensure that each identifier is configured only once." + ) + + configs = [io.config for io in drivers] gpio_configs: list[GpioDriverConfig] = [ io for io in configs if isinstance(io, GpioDriverConfig) @@ -232,16 +245,3 @@ def validate_device_address_space(ios: Iterable[CarlosDriver]): f"The I2C addresses {duplicate_i2c_addresses} are configured more than " f"once. Please ensure that each I2C address is configured only once." ) - - # Ensure all identifiers are unique - seen_identifiers = set() - duplicate_identifiers = [ - io.identifier - for io in configs - if io.identifier in seen_identifiers or seen_identifiers.add(io.identifier) # type: ignore[func-returns-value] # noqa: E501 - ] - if duplicate_identifiers: - raise ValueError( - f"The identifiers {duplicate_identifiers} are configured more than " - f"once. Please ensure that each identifier is configured only once." - ) diff --git a/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py b/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py index d52d0bdc..cb5558c9 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py @@ -1,9 +1,16 @@ +from contextlib import nullcontext from typing import Self import pytest from pydantic import BaseModel -from .driver import AnalogInput, DigitalOutput, DriverFactory +from .driver import ( + AnalogInput, + CarlosDriver, + DigitalOutput, + DriverFactory, + validate_device_address_space, +) from .driver_config import GpioDriverConfig DRIVER_MODULE = __name__ @@ -19,7 +26,7 @@ identifier="digital-output-test", driver_module=DRIVER_MODULE, direction="output", - pin=13, + pin=14, ) @@ -133,3 +140,66 @@ def test_driver_factory(): assert isinstance( driver.config, GpioDriverConfig ), "Config should be an instance of GpioDriverConfig." + + +@pytest.mark.parametrize( + "drivers, expected_exception", + [ + pytest.param([AnalogInputTest(ANALOG_INPUT_CONFIG)], None, id="valid-single"), + pytest.param( + [ + AnalogInputTest(ANALOG_INPUT_CONFIG), + DigitalOutputTest(DIGITAL_OUTPUT_CONFIG), + ], + None, + id="valid-multiple", + ), + pytest.param( + [ + AnalogInputTest( + GpioDriverConfig( + identifier="test", + pin=2, + direction="input", + driver_module=DRIVER_MODULE, + ) + ) + ], + None, + id="valid-i2c-ping-used", + ), + pytest.param( + [ + AnalogInputTest(ANALOG_INPUT_CONFIG), + AnalogInputTest(ANALOG_INPUT_CONFIG), + ], + ValueError, + id="duplicate-identifier", + ), + pytest.param( + [ + AnalogInputTest(ANALOG_INPUT_CONFIG), + AnalogInputTest( + ANALOG_INPUT_CONFIG.model_copy( + update={"identifier": "new-identifier"} + ) + ), + ], + ValueError, + id="duplicate-identifier-gpio-pin", + ), + ], +) +def test_validate_device_address_space( + drivers: list[CarlosDriver], expected_exception: type[Exception] | None +): + """This method ensures that the validate_device_address_space() works + as expected.""" + + if expected_exception is not None: + context = pytest.raises(expected_exception) + else: + context = nullcontext() + + with context: + validate_device_address_space(drivers) diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index ac28dcb6..dc000064 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -66,7 +66,7 @@ def test(): # pragma: no cover exceptions = {} results = {} - for driver in DriverManager().setup().ios: + for driver in DriverManager().setup().drivers: console.print(f"[cyan]Testing {driver} ... [/cyan]", end="") try: result = driver.test() From 0915c67163755d557ce91a60a386ce08a8bea963 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 20:02:03 +0200 Subject: [PATCH 52/57] full test coverage --- .../carlos/edge/device/protocol/gpio.py | 2 +- .../carlos/edge/interface/device/driver.py | 2 +- .../edge/interface/device/driver_test.py | 47 ++++++++++++++++++- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/protocol/gpio.py b/lib/py_edge_device/carlos/edge/device/protocol/gpio.py index 9c4f85a2..39c612dd 100644 --- a/lib/py_edge_device/carlos/edge/device/protocol/gpio.py +++ b/lib/py_edge_device/carlos/edge/device/protocol/gpio.py @@ -7,7 +7,7 @@ from RPi import GPIO # type: ignore except ImportError: warnings.warn( - "RPi.GPIO not available. Fallback tom mocked GPIO instead. " + "RPi.GPIO not available. Fallback to mocked GPIO instead. " f"{traceback.format_exc()}" ) from ._gpio_mock import GPIO # type: ignore diff --git a/lib/py_edge_interface/carlos/edge/interface/device/driver.py b/lib/py_edge_interface/carlos/edge/interface/device/driver.py index 60520b7f..6ac759fa 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/driver.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver.py @@ -238,7 +238,7 @@ def validate_device_address_space(drivers: Iterable[CarlosDriver]): duplicate_i2c_addresses = [ i2c.address for i2c in i2c_configs - if i2c.address in i2c_configs or seen_addresses.add(i2c.address) # type: ignore[func-returns-value] # noqa: E501 + if i2c.address in seen_addresses or seen_addresses.add(i2c.address) # type: ignore[func-returns-value] # noqa: E501 ] if duplicate_i2c_addresses: raise ValueError( diff --git a/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py b/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py index cb5558c9..9301036e 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver_test.py @@ -1,4 +1,5 @@ from contextlib import nullcontext +from secrets import token_hex from typing import Self import pytest @@ -11,7 +12,7 @@ DriverFactory, validate_device_address_space, ) -from .driver_config import GpioDriverConfig +from .driver_config import GpioDriverConfig, I2cDriverConfig DRIVER_MODULE = __name__ @@ -188,6 +189,50 @@ def test_driver_factory(): ValueError, id="duplicate-identifier-gpio-pin", ), + pytest.param( + [ + AnalogInputTest( + I2cDriverConfig( + identifier=token_hex(4), + driver_module=DRIVER_MODULE, + direction="input", + address="0x04", + ) + ), + AnalogInputTest( + GpioDriverConfig( + identifier="test", + pin=2, + direction="input", + driver_module=DRIVER_MODULE, + ) + ), + ], + ValueError, + id="i2c-pin-used", + ), + pytest.param( + [ + AnalogInputTest( + I2cDriverConfig( + identifier=token_hex(4), + driver_module=DRIVER_MODULE, + direction="input", + address="0x04", + ) + ), + AnalogInputTest( + I2cDriverConfig( + identifier=token_hex(4), + driver_module=DRIVER_MODULE, + direction="input", + address="0x04", + ) + ), + ], + ValueError, + id="duplicate-i2c-address", + ), ], ) def test_validate_device_address_space( From 9649ce804d932c4dfb44ef2d931bbfe9c23d7812 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 20:15:16 +0200 Subject: [PATCH 53/57] fix naming and tests --- .../carlos/edge/device/config.py | 23 +- .../carlos/edge/device/config_test.py | 4 +- .../carlos/edge/device/runtime.py | 10 +- .../tests/test_data/device_config | 2 +- .../edge/interface/device/driver_config.py | 2 +- .../interface/device/driver_config_test.py | 5 - lib/py_edge_interface/poetry.lock | 264 +----------------- lib/py_edge_interface/pyproject.toml | 1 - 8 files changed, 26 insertions(+), 285 deletions(-) diff --git a/lib/py_edge_device/carlos/edge/device/config.py b/lib/py_edge_device/carlos/edge/device/config.py index 8fcfcdb9..bfbd8229 100644 --- a/lib/py_edge_device/carlos/edge/device/config.py +++ b/lib/py_edge_device/carlos/edge/device/config.py @@ -2,7 +2,7 @@ configuration of the application.""" __all__ = [ - "load_io", + "load_drivers", "read_config_file", "write_config_file", ] @@ -43,7 +43,7 @@ def write_config_file(path: Path, config: Config): ) -def load_io(config_dir: Path | None = None) -> list[CarlosDriver]: +def load_drivers(config_dir: Path | None = None) -> list[CarlosDriver]: """Reads the configuration from the default location.""" config_dir = config_dir or Path.cwd() @@ -52,11 +52,17 @@ def load_io(config_dir: Path | None = None) -> list[CarlosDriver]: factory = DriverFactory() - ios = [factory.build(config) for config in raw_config.get("io", [])] + try: + driver_configs = [factory.build(config) for config in raw_config["drivers"]] + except KeyError: # pragma: no cover + raise KeyError( + f"The configuration file {config_dir / CONFIG_FILE_NAME} must contain " + f"a 'drivers' key." + ) # We always want to have some device metrics - if not any(isinstance(io, DeviceMetrics) for io in ios): - ios.insert( + if not any(isinstance(driver, DeviceMetrics) for driver in driver_configs): + driver_configs.insert( 0, factory.build( DriverConfig( @@ -66,6 +72,9 @@ def load_io(config_dir: Path | None = None) -> list[CarlosDriver]: ), ) - logger.info(f"Loaded {len(ios)} IOs: {', '.join(str(io) for io in ios)}") + logger.info( + f"Loaded {len(driver_configs)} IOs: " + f"{', '.join(str(io) for io in driver_configs)}" + ) - return ios + return driver_configs diff --git a/lib/py_edge_device/carlos/edge/device/config_test.py b/lib/py_edge_device/carlos/edge/device/config_test.py index e772c395..2883bad7 100644 --- a/lib/py_edge_device/carlos/edge/device/config_test.py +++ b/lib/py_edge_device/carlos/edge/device/config_test.py @@ -3,7 +3,7 @@ import pytest from carlos.edge.interface.device import AnalogInput, DigitalOutput, GpioDriverConfig -from carlos.edge.device.config import load_io, read_config_file, write_config_file +from carlos.edge.device.config import load_drivers, read_config_file, write_config_file from tests.test_data import EXPECTED_IO_COUNT, TEST_DEVICE_WORKDIR @@ -30,7 +30,7 @@ def test_config_file_io(tmp_path: Path): def test_load_io(): """This test ensures that the IOs are loaded correctly.""" - io = load_io(config_dir=TEST_DEVICE_WORKDIR) + io = load_drivers(config_dir=TEST_DEVICE_WORKDIR) assert len(io) == EXPECTED_IO_COUNT, "The number of IOs does not match." diff --git a/lib/py_edge_device/carlos/edge/device/runtime.py b/lib/py_edge_device/carlos/edge/device/runtime.py index 5a6ff404..39ce1972 100644 --- a/lib/py_edge_device/carlos/edge/device/runtime.py +++ b/lib/py_edge_device/carlos/edge/device/runtime.py @@ -13,7 +13,7 @@ from loguru import logger from .communication import DeviceCommunicationHandler -from .config import load_io +from .config import load_drivers # We don't cover this in the unit tests. This needs to be tested in an integration test. @@ -75,14 +75,14 @@ class DriverManager: # pragma: no cover def __init__(self): - self.drivers = load_io() + self.drivers = load_drivers() validate_device_address_space(self.drivers) def setup(self) -> Self: """Sets up the I/O peripherals.""" - for io in self.drivers: - logger.debug(f"Setting up I/O peripheral {io}.") - io.setup() + for driver in self.drivers: + logger.debug(f"Setting up driver {driver}.") + driver.setup() return self diff --git a/lib/py_edge_device/tests/test_data/device_config b/lib/py_edge_device/tests/test_data/device_config index 4af27a32..f0c34133 100644 --- a/lib/py_edge_device/tests/test_data/device_config +++ b/lib/py_edge_device/tests/test_data/device_config @@ -1,4 +1,4 @@ -io: +drivers: - identifier: temp-humi-intern driver_module: dht11 pin: 4 diff --git a/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py b/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py index d5a9f803..3cf5661e 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver_config.py @@ -46,7 +46,7 @@ def _validate_driver_module(cls, value): importlib.import_module(abs_module) except ModuleNotFoundError: raise ValueError(f"The module {value} ({abs_module}) does not exist.") - value = abs_module + value = abs_module # pragma: no cover return value diff --git a/lib/py_edge_interface/carlos/edge/interface/device/driver_config_test.py b/lib/py_edge_interface/carlos/edge/interface/device/driver_config_test.py index b4b3280c..64d25aea 100644 --- a/lib/py_edge_interface/carlos/edge/interface/device/driver_config_test.py +++ b/lib/py_edge_interface/carlos/edge/interface/device/driver_config_test.py @@ -16,11 +16,6 @@ class TestDriverConfig: "driver_module, expected", [ pytest.param(VALID_DRIVER_MODULE, VALID_DRIVER_MODULE, id="valid module"), - pytest.param( - "relay", - "carlos.edge.device.driver.relay", - id="built-in relative module", - ), pytest.param("non_existing_module", ValueError, id="invalid module"), ], ) diff --git a/lib/py_edge_interface/poetry.lock b/lib/py_edge_interface/poetry.lock index ca0d3316..442d7d57 100644 --- a/lib/py_edge_interface/poetry.lock +++ b/lib/py_edge_interface/poetry.lock @@ -11,72 +11,6 @@ files = [ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, ] -[[package]] -name = "anyio" -version = "4.3.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.8" -files = [ - {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, - {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, -] - -[package.dependencies] -idna = ">=2.8" -sniffio = ">=1.1" - -[package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] - -[[package]] -name = "apscheduler" -version = "4.0.0a4" -description = "In-process task scheduler with Cron-like capabilities" -optional = false -python-versions = ">=3.8" -files = [ - {file = "APScheduler-4.0.0a4-py3-none-any.whl", hash = "sha256:29576119a82a4e91fbd20971d9225c88b073b8e23a7e6bda5f9d555fa8bcccfc"}, - {file = "APScheduler-4.0.0a4.tar.gz", hash = "sha256:c349f281ec505235e1a748b0954d1ab868acefab69815559d62e025686945e65"}, -] - -[package.dependencies] -anyio = ">=4.0,<5.0" -attrs = ">=21.3" -tenacity = ">=8.0,<9.0" -tzlocal = ">=3.0" - -[package.extras] -asyncpg = ["asyncpg (>=0.20)"] -cbor = ["cbor2 (>=5.0)"] -doc = ["sphinx", "sphinx-autodoc-typehints", "sphinx-rtd-theme (>=1.3.0)", "sphinx-tabs (>=3.3.1)"] -mongodb = ["pymongo (>=4)"] -mqtt = ["paho-mqtt (>=1.5)"] -redis = ["redis (>=4.4.0)"] -sqlalchemy = ["sqlalchemy (>=2.0.19)"] -test = ["APScheduler[cbor,mongodb,mqtt,redis,sqlalchemy]", "PySide6 (>=6.6)", "aiosqlite (>=0.19)", "anyio[trio]", "asyncmy (>=0.2.5)", "asyncpg (>=0.20)", "coverage (>=7)", "freezegun", "paho-mqtt (>=1.5)", "psycopg", "pymongo (>=4)", "pymysql[rsa]", "pytest (>=7.4.0)", "pytest-freezer", "pytest-lazy-fixture", "pytest-mock", "uwsgi"] - -[[package]] -name = "attrs" -version = "23.2.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] - [[package]] name = "black" version = "24.4.0" @@ -146,30 +80,6 @@ dev = ["generate-changelog (>=0.7.6)", "git-fame (>=1.12.2)", "pip-tools", "pre- docs = ["black", "markdown-customblocks", "mdx-truly-sane-lists", "mkdocs", "mkdocs-click", "mkdocs-drawio", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkdocs-git-committers-plugin", "mkdocs-git-revision-date-localized-plugin", "mkdocs-include-markdown-plugin", "mkdocs-literate-nav", "mkdocs-material", "mkdocstrings[python]", "python-frontmatter"] test = ["coverage", "freezegun", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytest-sugar"] -[[package]] -name = "carlos-edge-device" -version = "0.1.2" -description = "The library for the edge device of the carlos project." -optional = false -python-versions = ">=3.11,<3.12" -files = [] -develop = false - -[package.dependencies] -apscheduler = "^4.0.0a4" -"carlos.edge.interface" = {path = "../py_edge_interface"} -loguru = "^0.7.2" -psutil = "^5.9.8" -pydantic = "^2.6.4" -pyyaml = "^6.0.1" -rpi-gpio = {version = "^0.7.1", markers = "platform_machine == \"armv7l\" or platform_machine == \"aarch64\""} -semver = "^3.0.2" -smbus2 = "^0.4.3" - -[package.source] -type = "directory" -url = "../py_edge_device" - [[package]] name = "certifi" version = "2024.2.2" @@ -720,34 +630,6 @@ files = [ [package.dependencies] wcwidth = "*" -[[package]] -name = "psutil" -version = "5.9.8" -description = "Cross-platform lib for process and system monitoring in Python." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -files = [ - {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, - {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, - {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, - {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, - {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, - {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, - {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, - {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, - {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, - {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, - {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, - {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, - {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, - {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, - {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, - {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, -] - -[package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] - [[package]] name = "pydantic" version = "2.7.0" @@ -996,55 +878,6 @@ files = [ {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, ] -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - [[package]] name = "questionary" version = "2.0.1" @@ -1117,21 +950,6 @@ typing-extensions = "*" [package.extras] dev = ["flake8", "flake8-docstrings", "mypy", "packaging", "pre-commit", "pytest", "pytest-cov", "types-setuptools"] -[[package]] -name = "rpi-gpio" -version = "0.7.1" -description = "A module to control Raspberry Pi GPIO channels" -optional = false -python-versions = "*" -files = [ - {file = "RPi.GPIO-0.7.1-cp27-cp27mu-linux_armv6l.whl", hash = "sha256:b86b66dc02faa5461b443a1e1f0c1d209d64ab5229696f32fb3b0215e0600c8c"}, - {file = "RPi.GPIO-0.7.1-cp310-cp310-linux_armv6l.whl", hash = "sha256:57b6c044ef5375a78c8dda27cdfadf329e76aa6943cd6cffbbbd345a9adf9ca5"}, - {file = "RPi.GPIO-0.7.1-cp37-cp37m-linux_armv6l.whl", hash = "sha256:77afb817b81331ce3049a4b8f94a85e41b7c404d8e56b61ac0f1eb75c3120868"}, - {file = "RPi.GPIO-0.7.1-cp38-cp38-linux_armv6l.whl", hash = "sha256:29226823da8b5ccb9001d795a944f2e00924eeae583490f0bc7317581172c624"}, - {file = "RPi.GPIO-0.7.1-cp39-cp39-linux_armv6l.whl", hash = "sha256:15311d3b063b71dee738cd26570effc9985a952454d162937c34e08c0fc99902"}, - {file = "RPi.GPIO-0.7.1.tar.gz", hash = "sha256:cd61c4b03c37b62bba4a5acfea9862749c33c618e0295e7e90aa4713fb373b70"}, -] - [[package]] name = "ruff" version = "0.3.7" @@ -1158,44 +976,6 @@ files = [ {file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"}, ] -[[package]] -name = "semver" -version = "3.0.2" -description = "Python helper for Semantic Versioning (https://semver.org)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4"}, - {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, -] - -[[package]] -name = "smbus2" -version = "0.4.3" -description = "smbus2 is a drop-in replacement for smbus-cffi/smbus-python in pure Python" -optional = false -python-versions = "*" -files = [ - {file = "smbus2-0.4.3-py2.py3-none-any.whl", hash = "sha256:a2fc29cfda4081ead2ed61ef2c4fc041d71dd40a8d917e85216f44786fca2d1d"}, - {file = "smbus2-0.4.3.tar.gz", hash = "sha256:36f2288a8e1a363cb7a7b2244ec98d880eb5a728a2494ac9c71e9de7bf6a803a"}, -] - -[package.extras] -docs = ["sphinx (>=1.5.3)"] -qa = ["flake8"] -test = ["mock", "nose"] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - [[package]] name = "sqlalchemy" version = "2.0.29" @@ -1284,20 +1064,6 @@ postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] sqlcipher = ["sqlcipher3_binary"] -[[package]] -name = "tenacity" -version = "8.2.3" -description = "Retry code until it succeeds" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"}, - {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"}, -] - -[package.extras] -doc = ["reno", "sphinx", "tornado (>=4.5)"] - [[package]] name = "tomlkit" version = "0.12.4" @@ -1320,34 +1086,6 @@ files = [ {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] -[[package]] -name = "tzdata" -version = "2024.1" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, -] - -[[package]] -name = "tzlocal" -version = "5.2" -description = "tzinfo object for the local timezone" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, - {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, -] - -[package.dependencies] -tzdata = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] - [[package]] name = "urllib3" version = "2.2.1" @@ -1409,4 +1147,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.12" -content-hash = "d3719212f8bab03aa3b13302be4af961e73bff6d58b5d6468b08c11517951523" +content-hash = "035adfa2f1c228e2aa1194533693e7b874ffc5f9f486782662cc7cb4299679d9" diff --git a/lib/py_edge_interface/pyproject.toml b/lib/py_edge_interface/pyproject.toml index 3cb3393c..e3b3746f 100644 --- a/lib/py_edge_interface/pyproject.toml +++ b/lib/py_edge_interface/pyproject.toml @@ -14,7 +14,6 @@ pydantic = "^2.6.4" [tool.poetry.group.dev.dependencies] "devtools" = {path = "../py_dev_dependencies"} -"carlos.edge.device" = {path = "../../lib/py_edge_device"} [tool.poetry.plugins.pytest11] qmulus_core = "carlos.edge.interface.plugin_pytest" From e10478fc9a28b896ed9f663c1d6e7ecd0eff26c2 Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 20:44:24 +0200 Subject: [PATCH 54/57] nice looking tests --- services/device/device/cli/config.py | 70 +++++++++++++++++----------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index dc000064..1f87484d 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -3,11 +3,18 @@ from typing import TypeVar import typer +from rich.live import Live +from rich.panel import Panel +from rich.pretty import Pretty +from rich.rule import Rule +from rich.spinner import Spinner +from rich.traceback import Traceback + from carlos.edge.device.runtime import DriverManager from pydantic import BaseModel from pydantic_core import PydanticUndefinedType from rich import print, print_json -from rich.console import Console +from rich.console import Console, Group from device.connection import ( ConnectionSettings, @@ -64,28 +71,39 @@ def show(): def test(): # pragma: no cover """Tests the io peripherals.""" - exceptions = {} - results = {} - for driver in DriverManager().setup().drivers: - console.print(f"[cyan]Testing {driver} ... [/cyan]", end="") - try: - result = driver.test() - console.print("[green]passed[/green]") - if result: - results[driver.identifier] = result - except Exception as e: - console.print("[red]failed[/red]") - console.print_exception() - exceptions[driver.identifier] = e - - if results: - console.print("\nThe following IO peripherals returned data:") - for identifier, result in results.items(): - console.print(f"{identifier}:") - console.print(result) - - if exceptions: - console.print("\nThe following IO peripherals [red]failed[/red]:") - for identifier, exception in exceptions.items(): - console.print(f"[red]{identifier}[/red]:") - console.print(exception) + driver_result_ui = [] + failed = [] + passed_cnt = 0 + with Live(Group(), refresh_per_second=4) as live: + for driver in DriverManager().setup().drivers: + driver_result_ui.append( + Panel( + Spinner(name="aesthetic", text="testing..."), + padding=(1, 2), + title=str(driver), + title_align="left", + subtitle="testing", + subtitle_align="right", + ) + ) + live.update(Group(*driver_result_ui)) + + try: + result = driver.test() + driver_result_ui[-1].renderable = Pretty(result) + driver_result_ui[-1].subtitle = "[green]passed[/green]" + passed_cnt += 1 + except Exception as e: + failed.append(driver) + driver_result_ui[-1].renderable = Traceback.from_exception( + type(e), e, e.__traceback__ + ) + driver_result_ui[-1].subtitle = "[red]failed[/red]" + + conclusions = [] + if passed_cnt > 0: + conclusions.append(f"[green]{passed_cnt} passed[/green]") + if failed: + conclusions.append(f"[red]{len(failed)} failed[/red]") + + live.update(Group(*driver_result_ui, Rule(", ".join(conclusions)))) \ No newline at end of file From a2de3164e41f3797f67125b79ebba386a7558ff9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 19 Apr 2024 18:45:20 +0000 Subject: [PATCH 55/57] update services/device --- services/device/device/cli/config.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index 1f87484d..f91e81f2 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -3,6 +3,11 @@ from typing import TypeVar import typer +from carlos.edge.device.runtime import DriverManager +from pydantic import BaseModel +from pydantic_core import PydanticUndefinedType +from rich import print, print_json +from rich.console import Console, Group from rich.live import Live from rich.panel import Panel from rich.pretty import Pretty @@ -10,12 +15,6 @@ from rich.spinner import Spinner from rich.traceback import Traceback -from carlos.edge.device.runtime import DriverManager -from pydantic import BaseModel -from pydantic_core import PydanticUndefinedType -from rich import print, print_json -from rich.console import Console, Group - from device.connection import ( ConnectionSettings, read_connection_settings, @@ -106,4 +105,4 @@ def test(): # pragma: no cover if failed: conclusions.append(f"[red]{len(failed)} failed[/red]") - live.update(Group(*driver_result_ui, Rule(", ".join(conclusions)))) \ No newline at end of file + live.update(Group(*driver_result_ui, Rule(", ".join(conclusions)))) From 9bb43130220b0423097a3aed6da4f3e004138c8c Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 20:47:29 +0200 Subject: [PATCH 56/57] format --- services/device/device/cli/config.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index 1f87484d..38016838 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -3,6 +3,11 @@ from typing import TypeVar import typer +from carlos.edge.device.runtime import DriverManager +from pydantic import BaseModel +from pydantic_core import PydanticUndefinedType +from rich import print, print_json +from rich.console import Console, Group from rich.live import Live from rich.panel import Panel from rich.pretty import Pretty @@ -10,12 +15,6 @@ from rich.spinner import Spinner from rich.traceback import Traceback -from carlos.edge.device.runtime import DriverManager -from pydantic import BaseModel -from pydantic_core import PydanticUndefinedType -from rich import print, print_json -from rich.console import Console, Group - from device.connection import ( ConnectionSettings, read_connection_settings, @@ -90,7 +89,9 @@ def test(): # pragma: no cover try: result = driver.test() - driver_result_ui[-1].renderable = Pretty(result) + driver_result_ui[-1].renderable = Pretty( + result or "[green]passed[/green]" + ) driver_result_ui[-1].subtitle = "[green]passed[/green]" passed_cnt += 1 except Exception as e: @@ -106,4 +107,4 @@ def test(): # pragma: no cover if failed: conclusions.append(f"[red]{len(failed)} failed[/red]") - live.update(Group(*driver_result_ui, Rule(", ".join(conclusions)))) \ No newline at end of file + live.update(Group(*driver_result_ui, Rule(", ".join(conclusions)))) From 06042379d0ee0971e621b5d7f0a801e14abf20fc Mon Sep 17 00:00:00 2001 From: flxdot Date: Fri, 19 Apr 2024 20:48:50 +0200 Subject: [PATCH 57/57] format --- services/device/device/cli/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/device/device/cli/config.py b/services/device/device/cli/config.py index 38016838..b634b6f4 100644 --- a/services/device/device/cli/config.py +++ b/services/device/device/cli/config.py @@ -89,8 +89,8 @@ def test(): # pragma: no cover try: result = driver.test() - driver_result_ui[-1].renderable = Pretty( - result or "[green]passed[/green]" + driver_result_ui[-1].renderable = ( + Pretty(result) if result else "[green]passed[/green]" ) driver_result_ui[-1].subtitle = "[green]passed[/green]" passed_cnt += 1