From f9505c2cde9f01cb706d0991dffeae450cbc16c9 Mon Sep 17 00:00:00 2001 From: jpk Date: Thu, 18 May 2023 19:23:56 +0200 Subject: [PATCH] [WIP] PoC for CLI event handling (not compatible with textual) --- blatted/cli/__init__.py | 19 +++-- blatted/events/__init__.py | 3 + blatted/events/event.py | 16 ++++ blatted/tools/ble/models.py | 2 +- blatted/tools/ble/monitor.py | 2 +- blatted/tools/ble/scanner.py | 48 +++++++++--- blatted/tools/context.py | 18 +++++ poetry.lock | 141 ++++++++++++++++++++++++++++++++++- pyproject.toml | 2 + 9 files changed, 229 insertions(+), 22 deletions(-) create mode 100644 blatted/events/__init__.py create mode 100644 blatted/events/event.py create mode 100644 blatted/tools/context.py diff --git a/blatted/cli/__init__.py b/blatted/cli/__init__.py index e7d1665..b0a60f9 100644 --- a/blatted/cli/__init__.py +++ b/blatted/cli/__init__.py @@ -1,24 +1,29 @@ -#!/usr/bin/env python3 - import click -from ..tools.ble import scanner, monitor +from ..tools import context +from ..tools.ble import monitor, scanner @click.command(name="scan") -def scanner_cmd(): +def scanner_cmd() -> None: scanner.run() @click.command(name="monitor") -def monitor_cmd(): +def monitor_cmd() -> None: monitor.run() -@click.group() -def main(): +@click.command(name="tui") +def tui_cmd() -> None: pass +@click.group() +def main() -> None: + context.set_environment(context.BlattedEnvironment.CLI) + + main.add_command(scanner_cmd) main.add_command(monitor_cmd) +main.add_command(tui_cmd) diff --git a/blatted/events/__init__.py b/blatted/events/__init__.py new file mode 100644 index 0000000..ea52005 --- /dev/null +++ b/blatted/events/__init__.py @@ -0,0 +1,3 @@ +from .event import DeviceDiscovered + +__all__ = ["DeviceDiscovered"] diff --git a/blatted/events/event.py b/blatted/events/event.py new file mode 100644 index 0000000..e46a20b --- /dev/null +++ b/blatted/events/event.py @@ -0,0 +1,16 @@ +from textual.message import Message +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + + +class DeviceDiscovered(Message): + def __init__(self, device: BLEDevice, adverisement_data: AdvertisementData) -> None: + self.device = device + self.adverisement_data = adverisement_data + super().__init__() + + def has_services(self) -> bool: + return len(self.advertising_data.service_uuids) > 0 + + def repr(self) -> str: + return f"{self.device.address} - {self.device.name}" diff --git a/blatted/tools/ble/models.py b/blatted/tools/ble/models.py index 0500631..896b5be 100644 --- a/blatted/tools/ble/models.py +++ b/blatted/tools/ble/models.py @@ -15,7 +15,7 @@ class DiscoveredDevices: devices: list[DiscoveredDevice] = field(default_factory=list) def add(self, device: DiscoveredDevice): - if not device in self.devices: + if device not in self.devices: self.devices.append(device) def count(self): diff --git a/blatted/tools/ble/monitor.py b/blatted/tools/ble/monitor.py index c3777ca..6129468 100644 --- a/blatted/tools/ble/monitor.py +++ b/blatted/tools/ble/monitor.py @@ -7,7 +7,7 @@ from icecream import ic async def monitor_services(filter: list[str] = []): - await asyncio.sleep(.1) + await asyncio.sleep(0.1) def run(uuid_filter: list[str] = []): diff --git a/blatted/tools/ble/scanner.py b/blatted/tools/ble/scanner.py index a4d6540..e631beb 100644 --- a/blatted/tools/ble/scanner.py +++ b/blatted/tools/ble/scanner.py @@ -1,31 +1,55 @@ import asyncio +from typing import Dict, Any import bleak.exc from bleak import BleakScanner from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData +from pydispatch import Dispatcher -from .models import DiscoveredDevice +from ...events.event import DeviceDiscovered +from ...tools import context -def discover_callback(device: BLEDevice, advertising_data: AdvertisementData): - if len(advertising_data.service_uuids) > 0: - discovered = DiscoveredDevice(device, advertising_data) - print( - f"[{advertising_data.rssi:-4d}] {device.address} - {device.name} [{discovered}]" - ) +class Scanner(Dispatcher): + _events_ = ["device_discovered"] + def __init__(self) -> None: + self.environment = context.get_environment() + self.devices: Dict[str, Any] = {} + self.bind(device_discovered=self.on_device_discovered) -async def discover_devices(): - stop_event = asyncio.Event() + def discover_callback( + self, device: BLEDevice, advertising_data: AdvertisementData + ) -> None: + if len(advertising_data.service_uuids) > 0: + #discovered = DiscoveredDevice(device, advertising_data) + discovered = DeviceDiscovered(device, advertising_data) + if context.get_environment() == context.BlattedEnvironment.CLI: + self.emit("device_discovered", data=discovered) + elif context.get_environment() == context.BlattedEnvironment.TUI: + pass # TODO Implement - async with BleakScanner(discover_callback): - await stop_event.wait() + def on_device_discovered(self, data: DeviceDiscovered) -> None: + if data.device.address not in self.devices: + print(f"new device discovered: {data.device}") + self.devices[data.device.address] = {"data": data, "seen": 1} + else: + self.devices[data.device.address]["seen"] += 1 + print( + f"device seen {self.devices[data.device.address]['seen']} times: {data.device}" + ) + + async def run(self) -> None: + stop_event = asyncio.Event() + + async with BleakScanner(self.discover_callback): + await stop_event.wait() def run(): print("scanner called") try: - asyncio.run(discover_devices()) + asyncio.run(Scanner().run()) except bleak.exc.BleakDBusError as exc: print(f"ERROR: {exc}") diff --git a/blatted/tools/context.py b/blatted/tools/context.py new file mode 100644 index 0000000..e073c39 --- /dev/null +++ b/blatted/tools/context.py @@ -0,0 +1,18 @@ +from contextvars import ContextVar +from enum import Enum + + +blatted_environment_var: ContextVar = ContextVar("blatted_environment") + + +class BlattedEnvironment(Enum): + CLI = "console line interface" + TUI = "terminal user interface" + + +def set_environment(mode: BlattedEnvironment) -> None: + blatted_environment_var.set(mode) + + +def get_environment() -> BlattedEnvironment: + return blatted_environment_var.get() diff --git a/poetry.lock b/poetry.lock index 36d5239..499055c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -238,6 +238,47 @@ files = [ [package.extras] license = ["ukkonen"] +[[package]] +name = "importlib-metadata" +version = "6.6.0" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "importlib_metadata-6.6.0-py3-none-any.whl", hash = "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed"}, + {file = "importlib_metadata-6.6.0.tar.gz", hash = "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] + +[[package]] +name = "linkify-it-py" +version = "2.0.2" +description = "Links recognition library with FULL unicode support." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "linkify-it-py-2.0.2.tar.gz", hash = "sha256:19f3060727842c254c808e99d465c80c49d2c7306788140987a1a7a29b0d6ad2"}, + {file = "linkify_it_py-2.0.2-py3-none-any.whl", hash = "sha256:a3a24428f6c96f27370d7fe61d2ac0be09017be5190d68d8658233171f1b6541"}, +] + +[package.dependencies] +uc-micro-py = "*" + +[package.extras] +benchmark = ["pytest", "pytest-benchmark"] +dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"] +doc = ["myst-parser", "sphinx", "sphinx-book-theme"] +test = ["coverage", "pytest", "pytest-cov"] + [[package]] name = "markdown-it-py" version = "2.2.0" @@ -251,6 +292,8 @@ files = [ ] [package.dependencies] +linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""} +mdit-py-plugins = {version = "*", optional = true, markers = "extra == \"plugins\""} mdurl = ">=0.1,<1.0" [package.extras] @@ -263,6 +306,26 @@ profiling = ["gprof2dot"] rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +[[package]] +name = "mdit-py-plugins" +version = "0.3.5" +description = "Collection of plugins for markdown-it-py" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"}, + {file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"}, +] + +[package.dependencies] +markdown-it-py = ">=1.0.0,<3.0.0" + +[package.extras] +code-style = ["pre-commit"] +rtd = ["attrs", "myst-parser (>=0.16.1,<0.17.0)", "sphinx-book-theme (>=0.1.0,<0.2.0)"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + [[package]] name = "mdurl" version = "0.1.2" @@ -415,6 +478,18 @@ files = [ [package.dependencies] pyobjc-core = ">=9.1.1" +[[package]] +name = "python-dispatch" +version = "0.2.1" +description = "Lightweight Event Handling" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "python-dispatch-0.2.1.tar.gz", hash = "sha256:ca166addfdedd11fef80a004b930503b30c5c2a6f23cec0395986fd7cc8a5f1c"}, + {file = "python_dispatch-0.2.1-py3-none-any.whl", hash = "sha256:43fb413a87404b212281bfc5733ef23d4b4ed3b7452faddb3c82517ea58634f1"}, +] + [[package]] name = "pyyaml" version = "6.0" @@ -513,6 +588,54 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "textual" +version = "0.25.0" +description = "Modern Text User Interface framework" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "textual-0.25.0-py3-none-any.whl", hash = "sha256:8258499a09793696e13a1d1e1391810916828015a91770abd7ce3d9ab64cfd9e"}, + {file = "textual-0.25.0.tar.gz", hash = "sha256:5e2d6320026dd8ff86e0d023fc4988071e80ec2e34de913bd63263a8301d664b"}, +] + +[package.dependencies] +importlib-metadata = ">=4.11.3" +markdown-it-py = {version = ">=2.1.0,<3.0.0", extras = ["linkify", "plugins"]} +rich = ">=13.3.3" +typing-extensions = ">=4.4.0,<5.0.0" + +[package.extras] +dev = ["aiohttp (>=3.8.1)", "click (>=8.1.2)", "msgpack (>=1.0.3)"] + +[[package]] +name = "typing-extensions" +version = "4.5.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, +] + +[[package]] +name = "uc-micro-py" +version = "1.0.2" +description = "Micro subset of unicode data files for linkify-it-py projects." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "uc-micro-py-1.0.2.tar.gz", hash = "sha256:30ae2ac9c49f39ac6dce743bd187fcd2b574b16ca095fa74cd9396795c954c54"}, + {file = "uc_micro_py-1.0.2-py3-none-any.whl", hash = "sha256:8c9110c309db9d9e87302e2f4ad2c3152770930d88ab385cd544e7a7e75f3de0"}, +] + +[package.extras] +test = ["coverage", "pytest", "pytest-cov"] + [[package]] name = "virtualenv" version = "20.23.0" @@ -534,7 +657,23 @@ platformdirs = ">=3.2,<4" docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.7.1)", "time-machine (>=2.9)"] +[[package]] +name = "zipp" +version = "3.15.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "1a257f20c24d028b39114e970d7e3156f4cac3daeae8e443f1381b168c7eeca5" +content-hash = "a8b87d0f21a27962db7d2999e0e30657c5ee0705abe98325a0a65466421d1349" diff --git a/pyproject.toml b/pyproject.toml index 4cce4bb..7654e55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,8 @@ python = "^3.10" bleak = "^0.20.2" click = "^8.1.3" rich = "^13.3.5" +textual = "^0.25.0" +python-dispatch = "^0.2.1" [tool.poetry.scripts] blatted = "blatted.cli:main"