diff --git a/.gitignore b/.gitignore index 5191513..11f42c2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ __pycache__/ *.py[cod] *.egg-info/ .mypy_cache/ +.venv/ dist/ diff --git a/README.md b/README.md index 30e992b..f93b208 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [Описание на русском языке](README_ru.md) -This is a library to work with the radiation detector and spectrometer [RadiaCode-101](https://scan-electronics.com/dosimeters/radiacode/radiacode-101). +This is a library to work with the radiation detector and spectrometer [RadiaCode](https://scan-electronics.com/dosimeters/radiacode/radiacode-101) (101, 102, 103). ***The project is still under development and not stable. Thus, the API might change in the future.*** @@ -33,10 +33,74 @@ $ python3 -m radiacode-examples.narodmon --bluetooth-mac 52:43:01:02:03:04 - install and run: ``` $ poetry install -$ poetry run python3 radiacode-examples/basic.py --bluetooth-mac 52:43:01:02:03:04 # or without --bluetooth-mac for USB connection + +$ poetry run python radiacode-examples/find-radiacode.py # To find your Radiacode over Bluetooth and discover its MAC address or UUID + +$ poetry run python radiacode-examples/basic.py --help # To see all options + +$ poetry run python radiacode-examples/basic.py --bluetooth-mac 52:43:01:02:03:04 # or without --bluetooth-mac for USB connection + +$ poetry run python radiacode-examples/basic.py --bluetooth-uuid 1EDA584E-652C-2011-1211-B213EFADEED0 # on Mac for bluetooth connection +``` + +### Examples +To install the dependencies required to run the examples: ```poetry install -E examples``` + +### Supported OS + +| OS | Bluetooth | USB | Notes | +| :--- | :---: | :---: | :--- | +| Mac OS (Silicon/Intel) | :white_check_mark: | :white_check_mark: | BT only with UUID, Mac OS does not expose MAC addresses | +| Linux | :white_check_mark: | :white_check_mark: | USB requires ```libusb```| +| Windows | :white_check_mark: | :white_check_mark: | USB required ```libusb``` | +| Windows (WSL) | :x: | :question: | WSL does not provide direct access to hardware| + +### Windows +Make sure ```libusb``` is installed: +- Download the [latest stable](https://github.com/libusb/libusb/releases) version +- Use [7-Zip](https://www.7-zip.org/download.html) to unpack it +- Pick the most recent ```libusb-1.0.dll``` file (for a *Windows 10/11 64-bit* installation, it should be in: ```libusb-1.x.xx\VS2022\MS64\dll```) +- Copy the ```.dll``` in your ```C:\Windows\System32``` folder +- Run scripts as usual + +### Mac Silicon/Intel +Make sure ```libusb``` is installed on your system, if you use [Homebrew](https://brew.sh/) you can run: +- ```brew install libusb``` + +*Note:* Mac OS does not expose Bluetooth MAC addresses anymore. In order to connect to your device via Bluetooth you will need to supply either its ```serial number``` (or part of it) or its ```UUID```. + +- **Serial number**: you can obtain it directly from the device, navigate to the **Info** menu, it should be in the form of: ```RC-10x-xxxxxx``` +- **UUID**: the UUID depends on both devices and it will change if you try to connect to the *Radiacode* from another computer (in this case, you will just need to rediscover) + +Discovering the UUID of your *Radiacode* is quite easy: +- Make sure the *Radiacode* is **disconnected** from other devices (such as your phone, so kill the *Radiacode app* and its background tasks) +- Run: ```poetry run python radiacode-examples/find-radiacode.py``` + +The script will return UUIDs for all available Radiacodes. + +### Linux +Make sure ```libusb``` is installed on your system. For *Debian/Ubuntu/Raspberry PI*: +- ```sudo apt install libusb``` + +You might have to tweak your ```udev``` configuration. On ```Debian/Ubuntu/Raspberry PI```: +- ```sudo nano /etc/udev/rules.d/99-com.rules``` + +Then add anywhere the following lines: + +``` +# Radiacode +SUBSYSTEM=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="f123", MODE="0666" ``` -## MacOS -The library used to communicate over Bluetooh (```bluepy```) is [not supported](https://github.com/IanHarvey/bluepy/issues/44) on MacOS. Only the USB connection is available on Apple devices. A ```USB Serial Number```, obtainable from the ```Device Info``` menu on the device itself, can be specified if more than one Radiacode is connected via USB at the same time. +Simply unplug and replug the Radiacode and run the scripts as per usual. + +#### Bluetooth +To enable Bluetooth on a *Raspberry Pi*: +- ```sudo apt install bluetooth pi-bluetooth bluez blueman``` + +On *Debian/Ubuntu* you should have all necessary packages by default, otherwise just try: +- ```sudo apt install bluetooth bluez``` + +## UUID and MAC Address +On `Windows` and `Linux` both `UUID` and `MAC addresses` can be used interchangeably, the library is flexible enough that it will try to establish a connection even if you supply a `UUID` where a `MAC address` is expected (e.g.: if you provide a `MAC address` while specifying `--bluetooth-uuid`). You will receive a warning anyway. -Make sure ```libusb``` is installed on your system, if you use ```Brew``` you can run: ```brew install libusb``` \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 7d6c2ad..af0ca46 100644 --- a/poetry.lock +++ b/poetry.lock @@ -114,7 +114,7 @@ frozenlist = ">=1.1.0" name = "async-timeout" version = "4.0.3" description = "Timeout context manager for asyncio programs" -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, @@ -141,13 +141,51 @@ 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 = "bluepy" -version = "1.3.0" -description = "Python module for interfacing with BLE devices through Bluez" +name = "bleak" +version = "0.22.1" +description = "Bluetooth Low Energy platform Agnostic Klient" +optional = false +python-versions = "<3.13,>=3.8" +files = [ + {file = "bleak-0.22.1-py3-none-any.whl", hash = "sha256:4afb5420847713535381ed2f04e7a70a1d1459153bb76e396a311964fde4aa4f"}, + {file = "bleak-0.22.1.tar.gz", hash = "sha256:73c2e774c22345e170d36a55a9dd06f6633c88b4184d5f86140a8224f12282d4"}, +] + +[package.dependencies] +async-timeout = {version = ">=3.0.0,<5", markers = "python_version < \"3.11\""} +bleak-winrt = {version = ">=1.2.0,<2.0.0", markers = "platform_system == \"Windows\" and python_version < \"3.12\""} +dbus-fast = {version = ">=1.83.0,<3", markers = "platform_system == \"Linux\""} +pyobjc-core = {version = ">=10.0,<11.0", markers = "platform_system == \"Darwin\""} +pyobjc-framework-CoreBluetooth = {version = ">=10.0,<11.0", markers = "platform_system == \"Darwin\""} +pyobjc-framework-libdispatch = {version = ">=10.0,<11.0", markers = "platform_system == \"Darwin\""} +typing-extensions = {version = ">=4.7.0", markers = "python_version < \"3.12\""} +winrt-runtime = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Devices.Bluetooth" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Devices.Bluetooth.Advertisement" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Devices.Bluetooth.GenericAttributeProfile" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Devices.Enumeration" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Foundation" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Foundation.Collections" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Storage.Streams" = {version = ">=2,<3", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} + +[[package]] +name = "bleak-winrt" +version = "1.2.0" +description = "Python WinRT bindings for Bleak" optional = false python-versions = "*" files = [ - {file = "bluepy-1.3.0.tar.gz", hash = "sha256:2a71edafe103565fb990256ff3624c1653036a837dfc90e1e32b839f83971cec"}, + {file = "bleak-winrt-1.2.0.tar.gz", hash = "sha256:0577d070251b9354fc6c45ffac57e39341ebb08ead014b1bdbd43e211d2ce1d6"}, + {file = "bleak_winrt-1.2.0-cp310-cp310-win32.whl", hash = "sha256:a2ae3054d6843ae0cfd3b94c83293a1dfd5804393977dd69bde91cb5099fc47c"}, + {file = "bleak_winrt-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:677df51dc825c6657b3ae94f00bd09b8ab88422b40d6a7bdbf7972a63bc44e9a"}, + {file = "bleak_winrt-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9449cdb942f22c9892bc1ada99e2ccce9bea8a8af1493e81fefb6de2cb3a7b80"}, + {file = "bleak_winrt-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:98c1b5a6a6c431ac7f76aa4285b752fe14a1c626bd8a1dfa56f66173ff120bee"}, + {file = "bleak_winrt-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:623ac511696e1f58d83cb9c431e32f613395f2199b3db7f125a3d872cab968a4"}, + {file = "bleak_winrt-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:13ab06dec55469cf51a2c187be7b630a7a2922e1ea9ac1998135974a7239b1e3"}, + {file = "bleak_winrt-1.2.0-cp38-cp38-win32.whl", hash = "sha256:5a36ff8cd53068c01a795a75d2c13054ddc5f99ce6de62c1a97cd343fc4d0727"}, + {file = "bleak_winrt-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:810c00726653a962256b7acd8edf81ab9e4a3c66e936a342ce4aec7dbd3a7263"}, + {file = "bleak_winrt-1.2.0-cp39-cp39-win32.whl", hash = "sha256:dd740047a08925bde54bec357391fcee595d7b8ca0c74c87170a5cbc3f97aa0a"}, + {file = "bleak_winrt-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:63130c11acfe75c504a79c01f9919e87f009f5e742bfc7b7a5c2a9c72bf591a7"}, ] [[package]] @@ -228,6 +266,49 @@ files = [ docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] tests = ["pytest", "pytest-cov", "pytest-xdist"] +[[package]] +name = "dbus-fast" +version = "2.21.2" +description = "A faster version of dbus-next" +optional = false +python-versions = "<4.0,>=3.7" +files = [ + {file = "dbus_fast-2.21.2-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:b5f79edcb0dd48e98b1a1e3e4a655fd0ecc2ba72275f9e8379e8655b4411edcc"}, + {file = "dbus_fast-2.21.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40aa9068759bbf7e062f074c965b391b95f18f897cc9be6eb906ee48a6f77724"}, + {file = "dbus_fast-2.21.2-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:d2406b838ccbda9bd49dda4a7620ce228da306cd8f9a3f8c9f42b2d792a491fb"}, + {file = "dbus_fast-2.21.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ed431895630135da9cec736326304f0833ac31919043efdbecf8f6c7bed40d05"}, + {file = "dbus_fast-2.21.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:90f09498ac91f0e6ddc7fa569e851a2b258a70917cd07ae8412ad5725ef1d411"}, + {file = "dbus_fast-2.21.2-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:b17f1eafeaa825e8933a5394157db9e0a24e65eac188a244dbbbc01dc23fde7a"}, + {file = "dbus_fast-2.21.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9f4191f7108f9433e5c017915e60ec57231aaf58c82fde6e20bd497998ebc97"}, + {file = "dbus_fast-2.21.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3b96a645cbd035f47f3b934130cd0ae977c043480ad7fe9838f78fdcb480c189"}, + {file = "dbus_fast-2.21.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bc696304ce0f5da374ddfb3e83273e9d89602a8f20e7fab57b079378f2cb5789"}, + {file = "dbus_fast-2.21.2-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:b5e2015a385f0b364eff1827b151313429d3148d2718d679bec8a9c67b78721a"}, + {file = "dbus_fast-2.21.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32efcbe276a4fdf6946450c512355e7ae22836cf3595d48c59330687cda52117"}, + {file = "dbus_fast-2.21.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:601c3c8796e7edd23bce0432e44ca8f0b85c48a17ab5258f57cd8fe815f9c07a"}, + {file = "dbus_fast-2.21.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:194899057b8382c1902c32e1a565a2d47bcc99e06aafe9d660348394532a4bf6"}, + {file = "dbus_fast-2.21.2-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:81ac390d4e26711b3ac46b3dd81a29bcbc1eddd4a408b336c67f0c94eb6d7ff0"}, + {file = "dbus_fast-2.21.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f056f2bfee24e87a4184202d3b108a56176344303bb1278988f13f5e90777da"}, + {file = "dbus_fast-2.21.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9ba884d102e069e105f22986fccf1d21776e6ced11f4b75aeddcc37e728a80fd"}, + {file = "dbus_fast-2.21.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:60d989030403cc1611105bec6a90df22967e523ae28486dee5f9bd644e37f797"}, + {file = "dbus_fast-2.21.2-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:aabe539f0e9961a1beb6e8c0078112a1a60de18958335678edb3f26021951ff9"}, + {file = "dbus_fast-2.21.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6edec4f92d32b9a288b38457a114086a0d5f5fdec9c3e9b7ff6052fd45963c1d"}, + {file = "dbus_fast-2.21.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37e6f717dedc299fc15ab8f5ec5b180725d2b896ba1aaef07c1921df0b7113a0"}, + {file = "dbus_fast-2.21.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eade5ed18327bf306b75e525ded98c08921e1b21d42e715b7f0a1371a7669168"}, + {file = "dbus_fast-2.21.2-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:2fd1be6967a92957f517dbd3755ee7cddc128ec840af2ef4ad6fb023a0dac74d"}, + {file = "dbus_fast-2.21.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5db0737471e60228c1a6aabecbf883c972f0b9e50bf7fc0878a8b35ebdf1d1e"}, + {file = "dbus_fast-2.21.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6c6f1fda6f318061a023d6da96ee50ad2d30c04557012a60a0f1abd39c2a8704"}, + {file = "dbus_fast-2.21.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0b78f2116fb745a7623c8e18d9c435bfe4732e4f9284a923c4b9a44ef68ae2d4"}, + {file = "dbus_fast-2.21.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:886ce5750d4e64636bd933f22513e9ba06b7ee9650f28699c553c162b52db666"}, + {file = "dbus_fast-2.21.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3159f1cecd4b86f565c01da787ad6eaa57e8ba210d355836fa849e4c0b1ee57"}, + {file = "dbus_fast-2.21.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:51279b69ac6b872208f3aa1b00b910dd9ef9c3d625b79eb378405dbd72a29cab"}, + {file = "dbus_fast-2.21.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29f07ef89e35b93afa87dea86abec2aff68802572944485250f50def15dc5ef8"}, + {file = "dbus_fast-2.21.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:38138fc5a24797cc443c6894d25497271ccf3399c8aa8cdba228a7bdda2d2921"}, + {file = "dbus_fast-2.21.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde99d085a330e8aed59535d808636f1f563cb08d12900d0e415508e6270a1d"}, + {file = "dbus_fast-2.21.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:c2bb0fd813bf3cafc6796d86d42cc8a9d37c2633d973dd963c3ad4c080d7061d"}, + {file = "dbus_fast-2.21.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:044eec5d0668d3229480094f5b2aefafb336afa6976d686bd0cd8770eee1bb2c"}, + {file = "dbus_fast-2.21.2.tar.gz", hash = "sha256:8645187b2e86c5141217adcb462d6dbecd37fb2ab8705f66b3773a66206ef83d"}, +] + [[package]] name = "fonttools" version = "4.51.0" @@ -883,6 +964,78 @@ files = [ [package.extras] twisted = ["twisted"] +[[package]] +name = "pyobjc-core" +version = "10.2" +description = "Python<->ObjC Interoperability Module" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyobjc-core-10.2.tar.gz", hash = "sha256:0153206e15d0e0d7abd53ee8a7fbaf5606602a032e177a028fc8589516a8771c"}, + {file = "pyobjc_core-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8eab50ce7f17017a0f1d68c3b7e88bb1bb033415fdff62b8e0a9ee4ab72f242"}, + {file = "pyobjc_core-10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f2115971463073426ab926416e17e5c16de5b90d1a1f2a2d8724637eb1c21308"}, + {file = "pyobjc_core-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a70546246177c23acb323c9324330e37638f1a0a3d13664abcba3bb75e43012c"}, + {file = "pyobjc_core-10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a9b5a215080d13bd7526031d21d5eb27a410780878d863f486053a0eba7ca9a5"}, + {file = "pyobjc_core-10.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:eb1ab700a44bcc4ceb125091dfaae0b998b767b49990df5fdc83eb58158d8e3f"}, + {file = "pyobjc_core-10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9a7163aff9c47d654f835f80361c1b112886ec754800d34e75d1e02ff52c3d7"}, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "10.2" +description = "Wrappers for the Cocoa frameworks on macOS" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyobjc-framework-Cocoa-10.2.tar.gz", hash = "sha256:6383141379636b13855dca1b39c032752862b829f93a49d7ddb35046abfdc035"}, + {file = "pyobjc_framework_Cocoa-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9227b4f271fda2250f5a88cbc686ff30ae02c0f923bb7854bb47972397496b2"}, + {file = "pyobjc_framework_Cocoa-10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6a6042b7703bdc33b7491959c715c1e810a3f8c7a560c94b36e00ef321480797"}, + {file = "pyobjc_framework_Cocoa-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:18886d5013cd7dc7ecd6e0df5134c767569b5247fc10a5e293c72ee3937b217b"}, + {file = "pyobjc_framework_Cocoa-10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ecf01400ee698d2e0ff4c907bcf9608d9d710e97203fbb97b37d208507a9362"}, + {file = "pyobjc_framework_Cocoa-10.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:0def036a7b24e3ae37a244c77bec96b7c9c8384bf6bb4d33369f0a0c8807a70d"}, + {file = "pyobjc_framework_Cocoa-10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f47ecc393bc1019c4b47e8653207188df784ac006ad54d8c2eb528906ff7013"}, +] + +[package.dependencies] +pyobjc-core = ">=10.2" + +[[package]] +name = "pyobjc-framework-corebluetooth" +version = "10.2" +description = "Wrappers for the framework CoreBluetooth on macOS" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyobjc-framework-CoreBluetooth-10.2.tar.gz", hash = "sha256:fb69d2c61082935b2b12827c1ba4bb22146eb3d251695fa1d58bbd5835260729"}, + {file = "pyobjc_framework_CoreBluetooth-10.2-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:6e118f08ae08289195841e0066389632206b68a8377ac384b30ac0c7e262b779"}, + {file = "pyobjc_framework_CoreBluetooth-10.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:411de4f937264b5e2935be25b78362c58118e2ab9f6a7af4d4d005813c458354"}, + {file = "pyobjc_framework_CoreBluetooth-10.2-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:81da4426a492089f9dd9ca50814766101f97574675782f7be7ce1a63197d497a"}, +] + +[package.dependencies] +pyobjc-core = ">=10.2" +pyobjc-framework-Cocoa = ">=10.2" + +[[package]] +name = "pyobjc-framework-libdispatch" +version = "10.2" +description = "Wrappers for libdispatch on macOS" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyobjc-framework-libdispatch-10.2.tar.gz", hash = "sha256:ae17602efbe628fa0432bcf436ee8137d2239a70669faefad420cd527e3ad567"}, + {file = "pyobjc_framework_libdispatch-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:955d3e3e5ee74f6707ab06cc76ad3fae27e78c180dea13f1b85e2659f9135889"}, + {file = "pyobjc_framework_libdispatch-10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:011736d708067d9b21a4722bae0ed776cbf84c8625fc81648de26228ca093f6b"}, + {file = "pyobjc_framework_libdispatch-10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:28c2a2ab2b4d2930f7c7865ad96c1157ad50ac93c58ffff64d889f769917a280"}, + {file = "pyobjc_framework_libdispatch-10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6cb0879e1f6773ad0bbeb82d495ad0d76d8c24b196a314ac9a6eab8eed1736e0"}, + {file = "pyobjc_framework_libdispatch-10.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:aa921cd469a1c2e20d8ba9118989fe4e827cbb98e947fd11ae0392f36db3afcc"}, + {file = "pyobjc_framework_libdispatch-10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6f3d57d24f81878d1b5dcb00a13f85465ede5b91589394f4f1b9dcf312f3bd99"}, +] + +[package.dependencies] +pyobjc-core = ">=10.2" +pyobjc-framework-Cocoa = ">=10.2" + [[package]] name = "pyparsing" version = "3.1.2" @@ -1041,6 +1194,224 @@ files = [ {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] +[[package]] +name = "winrt-runtime" +version = "2.0.1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt_runtime-2.0.1-cp310-cp310-win32.whl", hash = "sha256:b1d8c2c01b40755b8f546eaf01fef2c722af4fb6934e4ce7ad7e5eb7ba404846"}, + {file = "winrt_runtime-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:66bd7b98b5e2e2a0ae81089c26b5c284ee5f36603121584c82f2d1e0dfbfec37"}, + {file = "winrt_runtime-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:6136966a7c9f01c6cd55c7e2bc3b67573069b7f8b8ee910f1f791bece09ad597"}, + {file = "winrt_runtime-2.0.1-cp311-cp311-win32.whl", hash = "sha256:136142cecca5a87e13571277bace0ced0eee73f6d16dda967bc142bb7d4a0091"}, + {file = "winrt_runtime-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:d67360b744b1cc82efbe168ee21ed067483263f466aaab2f6321d9148b1b0552"}, + {file = "winrt_runtime-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:33d9b3cb99bf96082e883af0db97adc74a7f0fc7339f9ab9d28f64d59bba1212"}, + {file = "winrt_runtime-2.0.1-cp312-cp312-win32.whl", hash = "sha256:0947009e5f049bd7f3dd6284bb4161f644304877c9527c140f26e6daf9755767"}, + {file = "winrt_runtime-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:72e3ddcda15b35e77b4e334c6cdbf345e76b12e176510cc9c3b7c61a08fb6ce8"}, + {file = "winrt_runtime-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:e13d3ba43f3a63b506965b32353589968507f608c6c6484fa2684eb4cb9a230a"}, + {file = "winrt_runtime-2.0.1-cp39-cp39-win32.whl", hash = "sha256:a734860406b445325168b7fd9b7e98539cec6c24dc8095e3b6e6db84b2c1a656"}, + {file = "winrt_runtime-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:5dec5fc5e5f1be8fb7fd062ff30707500ff684b5fd5063fe4d7e9a196fdb9594"}, + {file = "winrt_runtime-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:c13f40456b78f25934392a72b87cca3e50380c06732afedb7a18698f281ccd25"}, + {file = "winrt_runtime-2.0.1.tar.gz", hash = "sha256:4d485fe7d2528ae220aca621a94aeafa28d938ed679599b8c4bbad0fc8877d9d"}, +] + +[[package]] +name = "winrt-windows-devices-bluetooth" +version = "2.0.1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt_Windows.Devices.Bluetooth-2.0.1-cp310-cp310-win32.whl", hash = "sha256:80ef50eb1d82cb869b6dc1f312bb5cd28a4c1f88946fc3abe7fc3c4f7ef2e4c8"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:838f66afb145e6a93fa5151f1e5883e55a781c5f1fdf37d2d903fcee7680565f"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:81971d961f41b71ca9999f3a6c03f35f50f9939bc144455ec6a7aea63aba8167"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.1-cp311-cp311-win32.whl", hash = "sha256:89042d64cc556ac1c49fef46b0a25ad969a66a2c473ddd5fd5f4cbd735c30c77"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:b6bac9fa687ab2ab4a98de2d7e96e21dfa7291bb388fcff4247096c099327cd6"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:03b461fd1d2005ff22f212ee418cc9d387502f1ea86ace9a347e81554dc95822"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.1-cp312-cp312-win32.whl", hash = "sha256:695e62296d87c676d385d427374d4f8452fc457b58d5ecd3118af01e3829370d"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:f80dcbd2f297e2789f367ad1fd4033e0d69057eb9dfc631327215915a95ba0b4"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:5a5bc9e541f23151255bd82ee3bffa319c35e9ba95879cdedf597cc8cc903f94"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.1-cp39-cp39-win32.whl", hash = "sha256:dffff7e6801b8e69e694b36fe1d147094fb6ac29ce54fd3ca3e52ab417473cc4"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:62bae806ecdf3021e1ec685d5a44012657c0961ca2027eeb1c37864f53577e51"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:7f3b102e9b4bea1915cc922b571e0c226956c161102d228ec1788e3caf4e226d"}, + {file = "winrt_windows_devices_bluetooth-2.0.1.tar.gz", hash = "sha256:c91b3f54bfe1ed7e1e597566b83a625d32efe397b21473668046ccb4b57f5a28"}, +] + +[package.dependencies] +winrt-runtime = "2.0.1" + +[package.extras] +all = ["winrt-Windows.Devices.Bluetooth.GenericAttributeProfile[all] (==2.0.1)", "winrt-Windows.Devices.Bluetooth.Rfcomm[all] (==2.0.1)", "winrt-Windows.Devices.Enumeration[all] (==2.0.1)", "winrt-Windows.Devices.Radios[all] (==2.0.1)", "winrt-Windows.Foundation.Collections[all] (==2.0.1)", "winrt-Windows.Foundation[all] (==2.0.1)", "winrt-Windows.Networking[all] (==2.0.1)", "winrt-Windows.Storage.Streams[all] (==2.0.1)"] + +[[package]] +name = "winrt-windows-devices-bluetooth-advertisement" +version = "2.0.1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.1-cp310-cp310-win32.whl", hash = "sha256:a63919f00fb15574443886be32295a1e95656eeda5c0a6299169338a276d03b0"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:266755af11ecb01c0c8a626da5072011ab4e1aea90426f80e1269107b8e8780a"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:856b00087a93763db157441eda78a308dc21cee816cbcf51ca7b4b07fde6cdb5"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.1-cp311-cp311-win32.whl", hash = "sha256:480050e8928da6c7f1f99a2b60206fbfb3252817fee0a9123142c9f8754e5687"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9272aa9ca77b356892218f32d1ffac0215fcdf331802d3bed842fd4d1448aeef"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:231af49fc2154a6a248d3e7c2a3ea131ac3fb870a1701c3fa5be65f258eb70e0"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3038f08fc6151055b4d11468b0dda3ee46ae6080b25221c4ea54c11b255a0f50"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:b547929477be00350118589f25dfc49f825e5df5da618afec465cede6af7e0a5"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:ea5c861e11bc6d565ab5fe6a4af6e6064c1812fee1d0e8f9c6dff2daafbcc046"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.1-cp39-cp39-win32.whl", hash = "sha256:86d11fd5c055f76eefac7f6cc02450832811503b83280e26a83613afe1d17c92"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:c8495ce12fda8fce3da130664917eb199d19ca1ebf7d5ab996f5df584b5e3a1f"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:0e91160a98e5b0fffae196982b5670e678ac919a6e14eb7e9798fdcbff45f8d2"}, + {file = "winrt_windows_devices_bluetooth_advertisement-2.0.1.tar.gz", hash = "sha256:130e6238a1897bfef98a711cdb1b02694fa0e18eb67d8fd4019a64a53685b331"}, +] + +[package.dependencies] +winrt-runtime = "2.0.1" + +[package.extras] +all = ["winrt-Windows.Devices.Bluetooth[all] (==2.0.1)", "winrt-Windows.Foundation.Collections[all] (==2.0.1)", "winrt-Windows.Foundation[all] (==2.0.1)", "winrt-Windows.Storage.Streams[all] (==2.0.1)"] + +[[package]] +name = "winrt-windows-devices-bluetooth-genericattributeprofile" +version = "2.0.1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.1-cp310-cp310-win32.whl", hash = "sha256:0a5118852dce4d50fd8d6c73ff3dc2c68403899b86060c0a85f7f0da284230cb"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:0695c73f0f20745c83ec9d7b5b8a6f55efd0df974dea81ad8382fad193f71275"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:8a6385cc8a1749f049f29037b2cfcce781bc80721ca4734de8ef0375b55ca1e5"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.1-cp311-cp311-win32.whl", hash = "sha256:858e48931713ddfb2ad52614bb87a653d91c30f602e335a3ed27daca15860b54"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:494000287b9b4e1b89fb4feb1379d95b2147dbf4bd4b1942f88c6c56afc2ba97"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:26270ea815c26df35c5c74ad7f10a99dd976e27031cc7316350a0c0395b19ee6"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.1-cp312-cp312-win32.whl", hash = "sha256:21e8c0f158adcf0b40c4b5bfd5144aa312e5edaaf759d6599c85c118ebf60214"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:b05d929b819e83e91299b0e5b937ea1ca524b15486791ee5b513ae026ef25efa"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:944215e5623d0c746a6c2e356dec36cf3a7281933f0819a3b4f2fbbddb4af382"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.1-cp39-cp39-win32.whl", hash = "sha256:3e2a54db384dcf05265a855a2548e2abd9b7726c8ec4b9ad06059606c5d90409"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:2bdbb55d4bef15c762a5d5b4e27b534146ec6580075ed9cc681e75e6ff0d5a97"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:01e74c76d4f16b4490d78c8c7509f2570c843366c1c6bf196a5b729520a31258"}, + {file = "winrt_windows_devices_bluetooth_genericattributeprofile-2.0.1.tar.gz", hash = "sha256:69d7dabd53fbf9acdc2d206def60f5c9777416a9d6911c3420be700aaff4e492"}, +] + +[package.dependencies] +winrt-runtime = "2.0.1" + +[package.extras] +all = ["winrt-Windows.Devices.Bluetooth[all] (==2.0.1)", "winrt-Windows.Devices.Enumeration[all] (==2.0.1)", "winrt-Windows.Foundation.Collections[all] (==2.0.1)", "winrt-Windows.Foundation[all] (==2.0.1)", "winrt-Windows.Storage.Streams[all] (==2.0.1)"] + +[[package]] +name = "winrt-windows-devices-enumeration" +version = "2.0.1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt_Windows.Devices.Enumeration-2.0.1-cp310-cp310-win32.whl", hash = "sha256:828456ed950d8b427d78dfedd54bf7514e9793019dbf6a5a8f725560be364578"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:217c9d14e94aea5e497f7aa8cd808e9255df98e28a12417194729debe0b77e65"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:751c20ad01a58a3bb5f273c3cf653475448d8f77f7331b96af7fc87204f1bc6a"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.1-cp311-cp311-win32.whl", hash = "sha256:8b28f4bc052a7442fdd7e3113021a264de972ca5421cc08ba53dd724f3826174"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:e3c0316f8487547fab36556b9ac94dec75551c70e0c977b92c4eb47c12d86bb0"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:572b0918f9babc9d2c31d8df800a988df2cf18492e462de602babc5d97639b16"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.1-cp312-cp312-win32.whl", hash = "sha256:6b6108466574846c969ba7d14e1d085948c1724cd087a494e1913315fb3ea4e0"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:8af4510ef393c3d97c71f082fc31412cf8b1cc8d8b74a30bab4144a5fd2edfe7"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:b3d36f47715550ba980fb1be43cdfbac17a833744cb2f08c634349a8a31feb9c"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.1-cp39-cp39-win32.whl", hash = "sha256:9301f5e00bd2562b063e0f6e0de6f0596b7fb3eabc443bd7e115772de6cc08f9"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:9999d93ae9441d35c564d498bb4d6767b593254a92b7c1559058a7450a0c304e"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:504ca45a9b90387a2f4f727dbbeefcf79beb013ac7a29081bb14c8ab13e10367"}, + {file = "winrt_windows_devices_enumeration-2.0.1.tar.gz", hash = "sha256:ed227dd22ece253db913de24e4fc5194d9f3272e2a5959a2450ae79e81bf7949"}, +] + +[package.dependencies] +winrt-runtime = "2.0.1" + +[package.extras] +all = ["winrt-Windows.ApplicationModel.Background[all] (==2.0.1)", "winrt-Windows.Foundation.Collections[all] (==2.0.1)", "winrt-Windows.Foundation[all] (==2.0.1)", "winrt-Windows.Security.Credentials[all] (==2.0.1)", "winrt-Windows.Storage.Streams[all] (==2.0.1)", "winrt-Windows.UI.Popups[all] (==2.0.1)", "winrt-Windows.UI[all] (==2.0.1)"] + +[[package]] +name = "winrt-windows-foundation" +version = "2.0.1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt_Windows.Foundation-2.0.1-cp310-cp310-win32.whl", hash = "sha256:f8cdc6f1f81e241a8a4d19f9d323828e61d75bd77fecfbe0c4d735e385326c4e"}, + {file = "winrt_Windows.Foundation-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:b8868c73642c66798c18ce8021796acb9beea59b6d2361344e8776c27ab847c7"}, + {file = "winrt_Windows.Foundation-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:4b5df8a9f69b15c71fe9b6f4f8e8589fe043d4d7ab843bb73607c7a4adde68f1"}, + {file = "winrt_Windows.Foundation-2.0.1-cp311-cp311-win32.whl", hash = "sha256:28ad6cd21126cc75cfb28527489e06699b36d8d6d5fdea5487e51c85ea9cc358"}, + {file = "winrt_Windows.Foundation-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:75d5a4974662ca11eab1438a711433951bef2e7db156b0c7ca34f47fbd19f117"}, + {file = "winrt_Windows.Foundation-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:d129a9fdfe5205bff2e9ccad705539fabd485ce6a8e47ded876aa664545b5216"}, + {file = "winrt_Windows.Foundation-2.0.1-cp312-cp312-win32.whl", hash = "sha256:a1dd4c93f435fd2f8f6e180af1cd2d3af8d22518b4c25c843e0b850b38e8be0f"}, + {file = "winrt_Windows.Foundation-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c54ac18fea4a488dae8a3261f6633c17ed2a632c35d15112caa9294f8c5560d"}, + {file = "winrt_Windows.Foundation-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:29f648a66a3e3285fcb5fdd1821582d60f6b3021e5dcc02acb008c8f48f15e7a"}, + {file = "winrt_Windows.Foundation-2.0.1-cp39-cp39-win32.whl", hash = "sha256:7abbf10666d6da5dbfb6a47125786a05dac267731a3d38feb8faddade9bf1151"}, + {file = "winrt_Windows.Foundation-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:aab18ad12de63a353ab1847aff3216ba4e5499e328da5edcb72c8007da6bdb02"}, + {file = "winrt_Windows.Foundation-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:bde9ecfc1c75410d669ee3124a84ba101d5a8ab1911807ad227658624fc22ffb"}, + {file = "winrt_windows_foundation-2.0.1.tar.gz", hash = "sha256:6e4da10cff652ac17740753c38ebe69565f5f970f60100106469b2e004ef312c"}, +] + +[package.dependencies] +winrt-runtime = "2.0.1" + +[package.extras] +all = ["winrt-Windows.Foundation.Collections[all] (==2.0.1)"] + +[[package]] +name = "winrt-windows-foundation-collections" +version = "2.0.1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt_Windows.Foundation.Collections-2.0.1-cp310-cp310-win32.whl", hash = "sha256:805f8d9a2f61276eb8e4284d439c61893c5acca0c17265f6dc10b8747c89bc39"}, + {file = "winrt_Windows.Foundation.Collections-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:2464c2b67e732f7e05497ea04ee43b9926743f10e8766c90a279270a3afe4b6a"}, + {file = "winrt_Windows.Foundation.Collections-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:ab1094c0091c573b1ecf72fd3cd8380d6eef9e6d0f59ccc5e676c697702eddc2"}, + {file = "winrt_Windows.Foundation.Collections-2.0.1-cp311-cp311-win32.whl", hash = "sha256:be0bd1b45252da5142b6ee80e3e488ad50931c6595e70674556ccad36080f2a9"}, + {file = "winrt_Windows.Foundation.Collections-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:a4f299a006266632240cc66ade8d82db6167a769fb1b5ec76cf22f2dfb43777a"}, + {file = "winrt_Windows.Foundation.Collections-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:84d66f1e2c8896534cfca80eaf4508e25d34e34b37b1e2eb4beb7462220edf78"}, + {file = "winrt_Windows.Foundation.Collections-2.0.1-cp312-cp312-win32.whl", hash = "sha256:d36a49ee53c8726148cac1920bf57a95b07eab576275b5efb0e97adea6fdacb2"}, + {file = "winrt_Windows.Foundation.Collections-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:c63e46569024e1504cc8e3b4f233e69162bc92e0428b4e92cb7dbc7cdc89db5f"}, + {file = "winrt_Windows.Foundation.Collections-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:5f5aac867f2b2fbc65e453c942bfde5bb158e60f47d2615455143fab335694e8"}, + {file = "winrt_Windows.Foundation.Collections-2.0.1-cp39-cp39-win32.whl", hash = "sha256:c26ab7b3342669dc09be62db5c5434e7194fb6eb1ec5b03fba1163f6b3e7b843"}, + {file = "winrt_Windows.Foundation.Collections-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:2f9bc7e28f3ade1c1f3113939dbf630bfef5e3c3018c039a404d7e4d39aae4cb"}, + {file = "winrt_Windows.Foundation.Collections-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:1f3e76f3298bec3938d94e4857c29af9776ec78112bdd09bb7794f06fd38bb13"}, + {file = "winrt_windows_foundation_collections-2.0.1.tar.gz", hash = "sha256:7d18955f161ba27d785c8fe2ef340f338b6edd2c5226fe2b005840e2a855e708"}, +] + +[package.dependencies] +winrt-runtime = "2.0.1" + +[package.extras] +all = ["winrt-Windows.Foundation[all] (==2.0.1)"] + +[[package]] +name = "winrt-windows-storage-streams" +version = "2.0.1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = false +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt_Windows.Storage.Streams-2.0.1-cp310-cp310-win32.whl", hash = "sha256:9e88593f17983b5714957c6f48e7ecdc8fb4410d57cd4b097aded722dc5243d3"}, + {file = "winrt_Windows.Storage.Streams-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:23bfecd91c2b4355f2281dc0cf8a29a32f1783d61f7df8162c29516a30df98a2"}, + {file = "winrt_Windows.Storage.Streams-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:4bbbb6476e25563395834ca0d18a674f3ae97dde8b10e1713f569ec60557ed92"}, + {file = "winrt_Windows.Storage.Streams-2.0.1-cp311-cp311-win32.whl", hash = "sha256:ed64b2215b6a1ff21c6849948e02fd7f31d1dbf81a4c25871d87dbb97029410e"}, + {file = "winrt_Windows.Storage.Streams-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ba46b0c7ca819598d2559c2374be9fb1e374a0d866a4fdca15fc1900d83e080"}, + {file = "winrt_Windows.Storage.Streams-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:0e802596bac1b59476a4a5a0cd68c7bd0483c4c3b4e4a2489768108729213113"}, + {file = "winrt_Windows.Storage.Streams-2.0.1-cp312-cp312-win32.whl", hash = "sha256:8d04759f1370514f95e486c7fad1ec33047dc8db275dd839bb906d79d2ee6088"}, + {file = "winrt_Windows.Storage.Streams-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:af2a459bdef54702972e05d9ba3bdc3583816404bddb79831f4a7b9a74e37ff1"}, + {file = "winrt_Windows.Storage.Streams-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:0a5446194fb88125569e293657f6bac926731d8ff6126f1b693848fbd2c72167"}, + {file = "winrt_Windows.Storage.Streams-2.0.1-cp39-cp39-win32.whl", hash = "sha256:f6dec418ad0118c258a1b2999fc8d4fc0d9575e6353a75a242ff8cc63c9b2146"}, + {file = "winrt_Windows.Storage.Streams-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:9fbc40f600ab44a45cda47b698bd8e494e80e221446a5958c4d8d59a8d46f117"}, + {file = "winrt_Windows.Storage.Streams-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:08059774c6d49d195ce00c3802d19364f418a6f3e42b94373621551792d2da60"}, + {file = "winrt_windows_storage_streams-2.0.1.tar.gz", hash = "sha256:3de8351ed3a9cfcfd1d028ce97ffe90bb95744f906eef025b06e7f4431943ee6"}, +] + +[package.dependencies] +winrt-runtime = "2.0.1" + +[package.extras] +all = ["winrt-Windows.Foundation.Collections[all] (==2.0.1)", "winrt-Windows.Foundation[all] (==2.0.1)", "winrt-Windows.Storage[all] (==2.0.1)", "winrt-Windows.System[all] (==2.0.1)"] + [[package]] name = "yarl" version = "1.9.4" @@ -1164,5 +1535,5 @@ examples = ["aiohttp", "matplotlib", "numpy", "prometheus-client", "pyyaml"] [metadata] lock-version = "2.0" -python-versions = "^3.9" -content-hash = "c1f54f063b1ab0b860b9f8bca0c295cacb07ed0547ad15975264f4dd4cd0e6c7" +python-versions = ">=3.9,<3.13" +content-hash = "5c4c27c493340ddb0a804cda7204e854413708947bf24ed1734ddda0123cd31a" diff --git a/pyproject.toml b/pyproject.toml index 9b99b87..ac7bd26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,8 +14,8 @@ classifiers = [ include = ["radiacode-examples/*"] [tool.poetry.dependencies] -python = "^3.9" -bluepy = { version = "^1.3", markers = "sys_platform != 'darwin'" } +python = ">=3.9,<3.13" +bleak = {version = "^0.22.1"} pyusb = "^1.2" aiohttp = {version = "^3.9", optional = true} prometheus-client = {version = "^0.19", optional = true} diff --git a/radiacode-examples/basic.py b/radiacode-examples/basic.py index 018aeee..8ffa169 100644 --- a/radiacode-examples/basic.py +++ b/radiacode-examples/basic.py @@ -1,48 +1,40 @@ import argparse import time -import platform from radiacode import RadiaCode from radiacode.transports.usb import DeviceNotFound as DeviceNotFoundUSB from radiacode.transports.bluetooth import DeviceNotFound as DeviceNotFoundBT -def main(): - parser = argparse.ArgumentParser() - - if platform.system() != 'Darwin': - parser.add_argument( - '--bluetooth-mac', type=str, required=False, help='bluetooth MAC address of radiascan device (e.g. 00:11:22:33:44:55)' - ) - - parser.add_argument( - '--serial', - type=str, - required=False, - help='serial number of radiascan device (e.g. "RC-10x-xxxxxx"). Useful in case of multiple devices.', - ) - - args = parser.parse_args() - - if hasattr(args, 'bluetooth_mac') and args.bluetooth_mac: - print(f'Connecting to Radiacode via Bluetooth (MAC address: {args.bluetooth_mac})') - - try: +def main(args: argparse.Namespace): + try: + if args.bluetooth_mac: + print(f'Connecting to Radiacode via Bluetooth (MAC address: {args.bluetooth_mac})') rc = RadiaCode(bluetooth_mac=args.bluetooth_mac) - except DeviceNotFoundBT as e: - print(e) - return - except ValueError as e: - print(e) - return - else: - print('Connecting to Radiacode via USB' + (f' (serial number: {args.serial})' if args.serial else '')) - - try: + elif args.bluetooth_uuid: + print(f'Connecting to Radiacode via Bluetooth (UUID: {args.bluetooth_uuid})') + rc = RadiaCode(bluetooth_uuid=args.bluetooth_uuid) + elif args.bluetooth_serial: + print(f'Connecting to Radiacode via Bluetooth (Serial: {args.bluetooth_serial})') + rc = RadiaCode(bluetooth_serial=args.bluetooth_serial) + elif args.serial: + print(f'Connecting to Radiacode via USB (Serial: {args.serial})') rc = RadiaCode(serial_number=args.serial) - except DeviceNotFoundUSB: - print('Device not found, check your USB connection') - return + else: + print('Connecting to Radiacode via USB') + rc = RadiaCode() + except DeviceNotFoundBT as e: + print(e) + return + except DeviceNotFoundUSB: + print('Radiacode not found, check your USB connection') + return + except ValueError as e: + print(e) + return + except Exception as e: + print(e) + return serial = rc.serial_number() print(f'### Serial number: {serial}') @@ -64,4 +56,31 @@ def main(): if __name__ == '__main__': - main() + parser = argparse.ArgumentParser() + + parser.add_argument( + '--bluetooth-mac', + type=str, + required=False, + help='Radiacode Bluetooth MAC address (e.g. 00:11:22:33:44:55). MacOS does not support BT MACs, use Serial Number or UUID instead.', + ) + parser.add_argument( + '--bluetooth-serial', + type=str, + required=False, + help='Connect via Bluetooth using Radiacode Serial (e.g. "RC-10x-xxxxxx").', + ) + parser.add_argument( + '--bluetooth-uuid', + type=str, + required=False, + help='Connect via Bluetooth using Radiacode UUID (e.g. "11111111-2222-3333-4444-56789ABCDEF").', + ) + parser.add_argument( + '--serial', type=str, required=False, help='Connect via USB using Radiacode Serial (e.g. "RC-10x-xxxxxx").' + ) + parser.add_argument('--usb', type=str, required=False, help='(default) Connect via USB to the first Radiacode available.') + + args = parser.parse_args() + + main(args) diff --git a/radiacode-examples/basic_async.py b/radiacode-examples/basic_async.py new file mode 100644 index 0000000..c0e4c70 --- /dev/null +++ b/radiacode-examples/basic_async.py @@ -0,0 +1,87 @@ +import argparse +import time +import asyncio + +from radiacode import RadiaCode +from radiacode.transports.usb import DeviceNotFound as DeviceNotFoundUSB +from radiacode.transports.bluetooth import DeviceNotFound as DeviceNotFoundBT + + +async def main(args: argparse.Namespace): + try: + if args.bluetooth_mac: + print(f'Connecting to Radiacode via Bluetooth (MAC address: {args.bluetooth_mac})') + rc = await RadiaCode.async_init(bluetooth_mac=args.bluetooth_mac) + elif args.bluetooth_uuid: + print(f'Connecting to Radiacode via Bluetooth (UUID: {args.bluetooth_uuid})') + rc = await RadiaCode.async_init(bluetooth_uuid=args.bluetooth_uuid) + elif args.bluetooth_serial: + print(f'Connecting to Radiacode via Bluetooth (Serial: {args.bluetooth_serial})') + rc = await RadiaCode.async_init(bluetooth_serial=args.bluetooth_serial) + elif args.serial: + print(f'Connecting to Radiacode via USB (Serial: {args.serial})') + rc = await RadiaCode.async_init(serial_number=args.serial) + else: + print('Connecting to Radiacode via USB') + rc = await RadiaCode.async_init() + except DeviceNotFoundBT as e: + print(e) + return + except DeviceNotFoundUSB: + print('Radiacode not found, check your USB connection') + return + except ValueError as e: + print(e) + return + except Exception as e: + print(e) + return + + serial = await rc.async_serial_number() + print(f'### Serial number: {serial}') + print('--------') + + fw_version = await rc.async_fw_version() + print(f'### Firmware: {fw_version}') + print('--------') + + spectrum = await rc.async_spectrum() + print(f'### Spectrum: {spectrum}') + print('--------') + + print('### DataBuf:') + while True: + for v in await rc.async_data_buf(): + print(v.dt.isoformat(), v) + time.sleep(2) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + + parser.add_argument( + '--bluetooth-mac', + type=str, + required=False, + help='Radiacode Bluetooth MAC address (e.g. 00:11:22:33:44:55). MacOS does not support BT MACs, use Serial Number or UUID instead.', + ) + parser.add_argument( + '--bluetooth-serial', + type=str, + required=False, + help='Connect via Bluetooth using Radiacode Serial (e.g. "RC-10x-xxxxxx").', + ) + parser.add_argument( + '--bluetooth-uuid', + type=str, + required=False, + help='Connect via Bluetooth using Radiacode UUID (e.g. "11111111-2222-3333-4444-56789ABCDEF").', + ) + parser.add_argument( + '--serial', type=str, required=False, help='Connect via USB using Radiacode Serial (e.g. "RC-10x-xxxxxx").' + ) + parser.add_argument('--usb', type=str, required=False, help='(default) Connect via USB to the first Radiacode available.') + + args = parser.parse_args() + + asyncio.run(main(args)) diff --git a/radiacode-examples/find-radiacode.py b/radiacode-examples/find-radiacode.py new file mode 100644 index 0000000..2596667 --- /dev/null +++ b/radiacode-examples/find-radiacode.py @@ -0,0 +1,23 @@ +""" + Simple scanner to find a Radiacode over bluetooth. + If a Radiacode is found, the library will print out both the device's serial number and the UUID. + On Mac the UUID is specific to a computer-radiacode pair. A scan from a different computer will return a different UUID. +""" +from radiacode.transports.bluetooth import Bluetooth +from radiacode.logger import Logger + + +def main(): + Logger.info('Looking for Radiacodes over Bluetooth') + radiacodes = Bluetooth()._scan() + + if len(radiacodes) == 0: + Logger.error( + 'No Radiacodes found, make sure Bluetooth is active on both your device and the Radiacode. Make sure the Radiacode is not connected to other devices.' + ) + else: + Logger.notify(f'{len(radiacodes)} Radiacode(s) found') + + +if __name__ == '__main__': + main() diff --git a/radiacode-examples/narodmon.py b/radiacode-examples/narodmon.py index e4b26ad..05e5cfa 100644 --- a/radiacode-examples/narodmon.py +++ b/radiacode-examples/narodmon.py @@ -1,10 +1,11 @@ import argparse import asyncio import time - import aiohttp from radiacode import RealTimeData, RadiaCode +from radiacode.transports.usb import DeviceNotFound as DeviceNotFoundUSB +from radiacode.transports.bluetooth import DeviceNotFound as DeviceNotFoundBT def sensors_data(rc_conn): @@ -47,20 +48,64 @@ async def send_data(d): def main(): parser = argparse.ArgumentParser() - parser.add_argument('--bluetooth-mac', type=str, required=True, help='MAC address of radiascan device') - parser.add_argument('--connection', choices=['usb', 'bluetooth'], default='bluetooth', help='device connection type') - parser.add_argument('--interval', type=int, required=False, default=600, help='send interval, seconds') + parser.add_argument( + '--bluetooth-mac', + type=str, + required=False, + help='Radiacode Bluetooth MAC address (e.g. 00:11:22:33:44:55). MacOS does not support BT MACs, use Serial Number or UUID instead.', + ) + parser.add_argument( + '--bluetooth-serial', + type=str, + required=False, + help='Connect via Bluetooth using Radiacode Serial (e.g. "RC-10x-xxxxxx").', + ) + parser.add_argument( + '--bluetooth-uuid', + type=str, + required=False, + help='Connect via Bluetooth using Radiacode UUID (e.g. "11111111-2222-3333-4444-56789ABCDEF").', + ) + parser.add_argument( + '--serial', type=str, required=False, help='Connect via USB using Radiacode Serial (e.g. "RC-10x-xxxxxx").' + ) + parser.add_argument('--usb', type=str, required=False, help='(default) Connect via USB to the first Radiacode available.') + parser.add_argument('--interval', type=int, required=False, default=600, help='Send interval, seconds') args = parser.parse_args() - if args.connection == 'usb': - print('will use USB connection') - rc_conn = RadiaCode() - else: - print('will use Bluetooth connection') - rc_conn = RadiaCode(bluetooth_mac=args.bluetooth_mac) + try: + if args.bluetooth_mac: + print(f'Connecting to Radiacode via Bluetooth (MAC address: {args.bluetooth_mac})') + rc = RadiaCode(bluetooth_mac=args.bluetooth_mac) + elif args.bluetooth_uuid: + print(f'Connecting to Radiacode via Bluetooth (UUID: {args.bluetooth_uuid})') + rc = RadiaCode(bluetooth_uuid=args.bluetooth_uuid) + elif args.bluetooth_serial: + print(f'Connecting to Radiacode via Bluetooth (Serial: {args.bluetooth_serial})') + rc = RadiaCode(bluetooth_serial=args.bluetooth_serial) + elif args.serial: + print(f'Connecting to Radiacode via USB (Serial: {args.serial})') + rc = RadiaCode(serial_number=args.serial) + else: + print('Connecting to Radiacode via USB') + rc = RadiaCode() + except DeviceNotFoundBT as e: + print(e) + return + except DeviceNotFoundUSB: + print('Radiacode not found, check your USB connection') + return + except ValueError as e: + print(e) + return + except Exception as e: + print(e) + return + + mac = args.bluetooth_mac.replace(':', '-') if args.bluetooth_mac else '00-00-00-00-00-00' device_data = { - 'mac': args.bluetooth_mac.replace(':', '-'), + 'mac': mac, 'name': 'RadiaCode-101', } @@ -69,11 +114,10 @@ def main(): 'devices': [ { **device_data, - 'sensors': sensors_data(rc_conn), + 'sensors': sensors_data(rc), }, ], } - print(f'Sending {d}') try: r = asyncio.run(send_data(d)) diff --git a/radiacode-examples/radiacode-exporter.py b/radiacode-examples/radiacode-exporter.py index 5934310..5dcfad2 100644 --- a/radiacode-examples/radiacode-exporter.py +++ b/radiacode-examples/radiacode-exporter.py @@ -4,21 +4,66 @@ import prometheus_client from radiacode import RealTimeData, RadiaCode +from radiacode.transports.usb import DeviceNotFound as DeviceNotFoundUSB +from radiacode.transports.bluetooth import DeviceNotFound as DeviceNotFoundBT def main(): parser = argparse.ArgumentParser() - parser.add_argument('--bluetooth-mac', type=str, required=False, help='bluetooth MAC address of radiascan device') + parser.add_argument( + '--bluetooth-mac', + type=str, + required=False, + help='Radiacode Bluetooth MAC address (e.g. 00:11:22:33:44:55). MacOS does not support BT MACs, use Serial Number or UUID instead.', + ) + parser.add_argument( + '--bluetooth-serial', + type=str, + required=False, + help='Connect via Bluetooth using Radiacode Serial (e.g. "RC-10x-xxxxxx").', + ) + parser.add_argument( + '--bluetooth-uuid', + type=str, + required=False, + help='Connect via Bluetooth using Radiacode UUID (e.g. "11111111-2222-3333-4444-56789ABCDEF").', + ) + parser.add_argument( + '--serial', type=str, required=False, help='Connect via USB using Radiacode Serial (e.g. "RC-10x-xxxxxx").' + ) parser.add_argument('--update-interval', type=int, default=3, required=False, help='update interval (seconds)') parser.add_argument('--port', type=int, default=5432, required=False, help='prometheus http port') + args = parser.parse_args() - if args.bluetooth_mac: - print('will use Bluetooth connection') - rc = RadiaCode(bluetooth_mac=args.bluetooth_mac) - else: - print('will use USB connection') - rc = RadiaCode() + try: + if args.bluetooth_mac: + print(f'Connecting to Radiacode via Bluetooth (MAC address: {args.bluetooth_mac})') + rc = RadiaCode(bluetooth_mac=args.bluetooth_mac) + elif args.bluetooth_uuid: + print(f'Connecting to Radiacode via Bluetooth (UUID: {args.bluetooth_uuid})') + rc = RadiaCode(bluetooth_uuid=args.bluetooth_uuid) + elif args.bluetooth_serial: + print(f'Connecting to Radiacode via Bluetooth (Serial: {args.bluetooth_serial})') + rc = RadiaCode(bluetooth_serial=args.bluetooth_serial) + elif args.serial: + print(f'Connecting to Radiacode via USB (Serial: {args.serial})') + rc = RadiaCode(serial_number=args.serial) + else: + print('Connecting to Radiacode via USB') + rc = RadiaCode() + except DeviceNotFoundBT as e: + print(e) + return + except DeviceNotFoundUSB: + print('Radiacode not found, check your USB connection') + return + except ValueError as e: + print(e) + return + except Exception as e: + print(e) + return serial = rc.serial_number() diff --git a/radiacode-examples/show-spectrum.py b/radiacode-examples/show-spectrum.py old mode 100755 new mode 100644 index 9b6895d..e19f692 --- a/radiacode-examples/show-spectrum.py +++ b/radiacode-examples/show-spectrum.py @@ -43,13 +43,15 @@ import time import numpy as np import yaml -import matplotlib as mpl import matplotlib.pyplot as plt import matplotlib.patches as patches + from radiacode import RadiaCode +from radiacode.transports.usb import DeviceNotFound as DeviceNotFoundUSB +from radiacode.transports.bluetooth import DeviceNotFound as DeviceNotFoundBT # set backend and matplotlib style -mpl.use('Qt5Agg') +# mpl.use('Qt5Agg') plt.style.use('dark_background') # some constants @@ -74,7 +76,7 @@ class appColors: auxline = 'red' -def plot_RC102Spectrum(): +def plot_RC102Spectrum(args: argparse.Namespace): # Helper functions for conversion of channel numbers to energies global a0, a1, a2 # calibration constants # approx. calibration, overwritten by first retrieved spectrum @@ -104,59 +106,58 @@ def on_mpl_window_closed(ax): # end helpers --------------------------------------- - # ------ - # parse command line arguments - # ------ - parser = argparse.ArgumentParser( - description='Read and display gamma energy spectrum from RadioCode 102, ' - + 'show differential and updated cumulative spectrum, ' - + 'optionally store data to file in yaml format.' - ) - parser.add_argument('-b', '--bluetooth-mac', type=str, required=False, help='bluetooth MAC address of device') - parser.add_argument('-s', '--serial-number', type=str, required=False, help='serial number of device') - parser.add_argument('-r', '--restart', action='store_true', help='restart spectrum accumulation') - parser.add_argument('-R', '--Reset', action='store_true', help='reset spectrum stored in device') - parser.add_argument('-q', '--quiet', action='store_true', help='no status output to terminal') - parser.add_argument('-i', '--interval', type=float, default=1.0, help='update interval') - parser.add_argument('-f', '--file', type=str, default='', help='file to store results') - parser.add_argument('-t', '--time', type=int, default=36000, help='run time in seconds') - parser.add_argument('-H', '--history', type=int, default=500, help='number of rate-history points to store in file') - args = parser.parse_args() - bluetooth_mac = args.bluetooth_mac + bluetooth_uuid = args.bluetooth_uuid + bluetooth_serial = args.bluetooth_serial serial_number = args.serial_number restart_accumulation = args.restart reset_device_spectrum = args.Reset quiet = args.quiet dt_wait = args.interval timestamp = time.strftime('%y%m%d-%H%M', time.localtime()) + print(args.file) + filename = args.file + '_' + timestamp + '.yaml' if args.file != '' else '' NHistory = args.history run_time = args.time rate_history = np.zeros(NHistory) - if not quiet: - print(f'\n *==* script {sys.argv[0]} executing') - if bluetooth_mac is not None: - print(f' connecting via Bluetooth, MAC {bluetooth_mac}') - elif serial_number is not None: - print(f' connect via USB to device with SN {serial_number}') - else: - print(' connect via USB') - # ------ # initialize and connect to RC10x device # ------ - rc = RadiaCode(bluetooth_mac=bluetooth_mac, serial_number=serial_number) + try: + rc = RadiaCode( + bluetooth_mac=bluetooth_mac, + bluetooth_uuid=bluetooth_uuid, + bluetooth_serial=bluetooth_serial, + serial_number=serial_number, + ) + except DeviceNotFoundBT as e: + print(e) + return + except DeviceNotFoundUSB: + print('Radiacode not found, check your USB connection') + return + except ValueError as e: + print(e) + return + except Exception as e: + print(e) + return + serial = rc.serial_number() fw_version = rc.fw_version() - status_flags = eval(rc.status().split(':')[1])[0] + status = rc.status() + status_flags = eval(status.split(':')[1])[0] a0, a1, a2 = rc.energy_calib() + # get initial spectrum and meta-data if reset_device_spectrum: rc.spectrum_reset() + spectrum = rc.spectrum() + # print(f'### Spectrum: {spectrum}') counts0 = np.asarray(spectrum.counts) NChannels = len(counts0) @@ -377,4 +378,53 @@ def on_mpl_window_closed(ax): if __name__ == '__main__': - plot_RC102Spectrum() + parser = argparse.ArgumentParser() + + # ------ + # parse command line arguments + # ------ + parser = argparse.ArgumentParser( + description='Read and display gamma energy spectrum from RadioCode 102, ' + + 'show differential and updated cumulative spectrum, ' + + 'optionally store data to file in yaml format.' + ) + parser.add_argument( + '-b', + '--bluetooth-mac', + type=str, + required=False, + help='Radiacode Bluetooth MAC address (e.g. 00:11:22:33:44:55). MacOS does not support BT MACs, use Serial Number or UUID instead.', + ) + parser.add_argument( + '-u', + '--bluetooth-uuid', + type=str, + required=False, + help='Connect via Bluetooth using Radiacode UUID (e.g. "11111111-2222-3333-4444-56789ABCDEF").', + ) + parser.add_argument( + '-e', + '--bluetooth-serial', + type=str, + required=False, + help='Connect via Bluetooth using Radiacode Serial (e.g. "RC-10x-xxxxxx").', + ) + parser.add_argument( + '-s', '--serial-number', type=str, required=False, help='Connect via USB using Radiacode Serial (e.g. "RC-10x-xxxxxx").' + ) + parser.add_argument('-r', '--restart', action='store_true', help='Restart spectrum accumulation') + parser.add_argument('-R', '--Reset', action='store_true', help='Reset spectrum stored in device') + parser.add_argument('-q', '--quiet', action='store_true', help='No status output to terminal') + parser.add_argument('-i', '--interval', type=float, default=1.0, help='Update interval (s)') + parser.add_argument( + '-f', + '--file', + type=str, + default='', + help='Filename to store results (.yaml extension will be automatically added to the name)', + ) + parser.add_argument('-t', '--time', type=int, default=36000, help='Run time in seconds') + parser.add_argument('-H', '--history', type=int, default=500, help='Number of rate-history points to store in file') + args = parser.parse_args() + + plot_RC102Spectrum(args) diff --git a/radiacode-examples/webserver.html b/radiacode-examples/webserver.html index 879acf9..8977f08 100644 --- a/radiacode-examples/webserver.html +++ b/radiacode-examples/webserver.html @@ -1,141 +1,197 @@ - + - -RadiaCode demo - - - + + RadiaCode demo + + + + -
-
- -
-
- - -
-
- - - - - -
-
- - - - -
+
Fetching data from Radiacode, please wait (it might take up to 1 minute over bluetooth)...
+
Error reading data, try restarting the application...
+
+
+ +
+
+ + +
+
+ + + + +
+
+ + + + +
+
+
+ + +
+
+
+ +
+ +
+
- - -
-
- - -
- diff --git a/radiacode-examples/webserver.py b/radiacode-examples/webserver.py index 687e8ac..2930128 100644 --- a/radiacode-examples/webserver.py +++ b/radiacode-examples/webserver.py @@ -4,7 +4,6 @@ import pathlib from aiohttp import web - from radiacode import RadiaCode, RealTimeData @@ -15,47 +14,61 @@ async def handle_index(request): async def handle_ws(request): ws = web.WebSocketResponse() await ws.prepare(request) - request.app.ws_clients.append(ws) - async for _ in ws: - pass - request.app.ws_clients.remove(ws) + request.app['ws_clients'].append(ws) + + try: + async for _ in ws: + pass + except Exception as e: + print(f'Unexpected error in websocket: {str(e)}') + finally: + request.app['ws_clients'].remove(ws) + return ws async def handle_spectrum(request): - cn = request.app.rc_conn + cn = request.app['rc_conn'] accum = request.query.get('accum') == 'true' - spectrum = cn.spectrum_accum() if accum else cn.spectrum() + + try: + spectrum = await (cn.async_spectrum_accum() if accum else cn.async_spectrum()) + except Exception as e: + print(f'Unexpected error while fetching data: {str(e)}') + return web.json_response({'error': str(e)}, status=500) + # apexcharts can't handle 0 in logarithmic view spectrum_data = [(channel, cnt if cnt > 0 else 0.5) for channel, cnt in enumerate(spectrum.counts)] - print('Spectrum updated') + return web.json_response( { 'coef': [spectrum.a0, spectrum.a1, spectrum.a2], 'duration': spectrum.duration.total_seconds(), 'series': [{'name': 'spectrum', 'data': spectrum_data}], - }, + } ) async def handle_spectrum_reset(request): - cn = request.app.rc_conn - cn.spectrum_reset() - print('Spectrum reset') - return web.json_response({}) + cn = request.app['rc_conn'] + await cn.async_spectrum_reset() + return web.json_response({'message': 'Spectrum reset'}) async def process(app): max_history_size = 128 history = [] + while True: - databuf = app.rc_conn.data_buf() + databuf = await app['rc_conn'].async_data_buf() + for v in databuf: if isinstance(v, RealTimeData): history.append(v) history.sort(key=lambda x: x.dt) history = history[-max_history_size:] + jdata = json.dumps( { 'series': [ @@ -70,37 +83,53 @@ async def process(app): ], }, ) - print(f'Rates updated, sending to {len(app.ws_clients)} connected clients') - await asyncio.gather(*[ws.send_str(jdata) for ws in app.ws_clients], asyncio.sleep(1.0)) + + print(f'Rates updated, sending to {len(app["ws_clients"])} connected clients') + await asyncio.gather(*(ws.send_str(jdata) for ws in app['ws_clients'])) + await asyncio.sleep(1.0) async def on_startup(app): - asyncio.create_task(process(app)) + app['process_task'] = asyncio.create_task(process(app)) + app['ws_clients'] = [] -if __name__ == '__main__': +async def init_connection(app): + if app['args'].bluetooth_mac: + print('will use Bluetooth connection via MAC address') + app['rc_conn'] = await RadiaCode.async_init(bluetooth_mac=app['args'].bluetooth_mac) + elif app['args'].bluetooth_uuid: + print('will use Bluetooth connection via UUID') + app['rc_conn'] = await RadiaCode.async_init(bluetooth_uuid=app['args'].bluetooth_uuid) + else: + print('will use USB connection') + app['rc_conn'] = await RadiaCode.async_init() + + +def main(): parser = argparse.ArgumentParser() - parser.add_argument('--bluetooth-mac', type=str, required=False, help='bluetooth MAC address of radiascan device') - parser.add_argument('--listen-host', type=str, required=False, default='0.0.0.0', help='listen host for webserver') - parser.add_argument('--listen-port', type=int, required=False, default=8080, help='listen port for webserver') + parser.add_argument('--bluetooth-mac', type=str, required=False, help='Bluetooth MAC address of radiascan device') + parser.add_argument('--bluetooth-uuid', type=str, required=False, help='Bluetooth UUID of radiascan device') + parser.add_argument('--listen-host', type=str, required=False, default='127.0.0.1', help='Listen host for webserver') + parser.add_argument('--listen-port', type=int, required=False, default=8080, help='Listen port for webserver') args = parser.parse_args() app = web.Application() - app.ws_clients = [] - if args.bluetooth_mac: - print('will use Bluetooth connection') - app.rc_conn = RadiaCode(bluetooth_mac=args.bluetooth_mac) - else: - print('will use USB connection') - app.rc_conn = RadiaCode() - + app['args'] = args + app.on_startup.append(init_connection) app.on_startup.append(on_startup) + app.add_routes( [ web.get('/', handle_index), web.get('/spectrum', handle_spectrum), web.post('/spectrum/reset', handle_spectrum_reset), web.get('/ws', handle_ws), - ], + ] ) + web.run_app(app, host=args.listen_host, port=args.listen_port) + + +if __name__ == '__main__': + main() diff --git a/radiacode/logger.py b/radiacode/logger.py new file mode 100644 index 0000000..7e02e1f --- /dev/null +++ b/radiacode/logger.py @@ -0,0 +1,35 @@ +from enum import Enum + + +class LogLevel(Enum): + Notification = ('notification', '[\033[1;32m\N{check mark}\033[0m]') + Error = ('error', '[\033[1;31m\N{aegean check mark}\033[0m]') + Info = ('info', '[\033[1;34m\N{information source}\033[0m]') + Warning = ('warning', '[\033[1;35m\N{warning sign}\033[0m]') + + +class Logger: + @staticmethod + def log(message): + print(message) + + @staticmethod + def notify(message): + Logger._log_level(message, LogLevel.Notification) + + @staticmethod + def error(message): + Logger._log_level(message, LogLevel.Error) + + @staticmethod + def info(message): + Logger._log_level(message, LogLevel.Info) + + @staticmethod + def warning(message): + Logger._log_level(message, LogLevel.Warning) + + @staticmethod + def _log_level(message, level): + prefix = level.value[1] + print(f'{prefix} {message}') diff --git a/radiacode/radiacode.py b/radiacode/radiacode.py index e3217c2..e1746c9 100644 --- a/radiacode/radiacode.py +++ b/radiacode/radiacode.py @@ -1,8 +1,10 @@ import datetime import struct import platform +import asyncio from typing import List, Optional, Union +from radiacode.logger import Logger from radiacode.bytes_buffer import BytesBuffer from radiacode.decoders.databuf import decode_VS_DATA_BUF from radiacode.decoders.spectrum import decode_RC_VS_SPECTRUM @@ -19,44 +21,208 @@ def spectrum_channel_to_energy(channel_number: int, a0: float, a1: float, a2: fl class RadiaCode: _connection: Union[Bluetooth, Usb] + """ + To use this class synchronously: + rc = Radiacode() + serial = rc.serial_number() + """ + def __init__( self, bluetooth_mac: Optional[str] = None, + bluetooth_serial: Optional[str] = None, + bluetooth_uuid: Optional[str] = None, serial_number: Optional[str] = None, - ignore_firmware_compatibility_check: bool = False, + ignore_firmware_compatibility_check: Optional[bool] = False, + async_init: Optional[bool] = False, ): self._seq = 0 + self._usb = False + self._async_init = async_init + self._ignore_firmware_compatibility_check = ignore_firmware_compatibility_check + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + if platform.system() == 'Darwin' and bluetooth_mac: + Logger.warning( + 'You would like to establish a bluetooth connection using a MAC address, but you appear to be on a Mac.' + ) + Logger.warning('Apple does not expose Bluetooth MAC addresses anymore, so this method will not work.') + Logger.warning('Try connecting to the device using another option (UUID or Serial).') - # Bluepy doesn't support MacOS: https://github.com/IanHarvey/bluepy/issues/44 - self._bt_supported = platform.system() != 'Darwin' + raise Exception('Exception: The chosen connection method does not work on this platform.') - if bluetooth_mac is not None and self._bt_supported is True: - self._connection = Bluetooth(bluetooth_mac) + if bluetooth_mac is not None: + self._connection = Bluetooth(bluetooth_mac=bluetooth_mac) + elif bluetooth_serial is not None: + self._connection = Bluetooth(bluetooth_serial=bluetooth_serial) + elif bluetooth_uuid is not None: + self._connection = Bluetooth(bluetooth_uuid=bluetooth_uuid) else: + self._usb = True + # Connect over USB self._connection = Usb(serial_number=serial_number) - # init - self.execute(b'\x07\x00', b'\x01\xff\x12\xff') + if self._async_init is False: + self.loop.run_until_complete(self._init_device()) + + """ + Use this method to create an asynchronous Radiacode() object: + rc = Radiacode.async_init() + serial = await rc.async_serial_number() + """ + + @classmethod + async def async_init( + cls, + bluetooth_mac: Optional[str] = None, + bluetooth_serial: Optional[str] = None, + bluetooth_uuid: Optional[str] = None, + serial_number: Optional[str] = None, + ignore_firmware_compatibility_check: Optional[bool] = False, + ): + if platform.system() == 'Darwin' and bluetooth_mac: + Logger.warning('You want to connect over bluetooth using a MAC address, but you appear to be on a Mac.') + Logger.warning('Apple does not expose Bluetooth MAC addresses anymore, so this method will not work.') + Logger.warning('Try connecting to the device using another option (UUID or Serial).') + + raise Exception('Exception: The chosen connection method does not work on this platform.') + + self = cls( + bluetooth_mac=bluetooth_mac, + bluetooth_serial=bluetooth_serial, + bluetooth_uuid=bluetooth_uuid, + serial_number=serial_number, + ignore_firmware_compatibility_check=ignore_firmware_compatibility_check, + async_init=True, + ) + + await self._init_device() + return self + + async def _init_device(self): + if self._usb is False: + # Connect over Bluetooth + await self._connection.connect() + + # Init + Logger.info('Initializing Radiacode...') + await self._async_execute(b'\x07\x00', b'\x01\xff\x12\xff') + self._base_time = datetime.datetime.now() - self.set_local_time(self._base_time) - self.device_time(0) + await self.async_set_local_time(self._base_time) + await self.async_device_time(0) - (_, (vmaj, vmin, _)) = self.fw_version() - if ignore_firmware_compatibility_check is False and vmaj < 4 or (vmaj == 4 and vmin < 8): + (_, (vmaj, vmin, _)) = await self.async_fw_version() + if self._ignore_firmware_compatibility_check is False and vmaj < 4 or (vmaj == 4 and vmin < 8): raise Exception( f'Incompatible firmware version {vmaj}.{vmin}, >=4.8 required. Upgrade device firmware or use radiacode==0.2.2' ) self._spectrum_format_version = 0 - for line in self.configuration().split('\n'): + lines = await self.async_configuration() + + for line in lines.split('\n'): if line.startswith('SpecFormatVersion'): self._spectrum_format_version = int(line.split('=')[1]) break + Logger.notify('Initialization completed') + def base_time(self) -> datetime.datetime: return self._base_time - def execute(self, reqtype: bytes, args: Optional[bytes] = None) -> BytesBuffer: + # def _execute(self, reqtype: bytes, args: Optional[bytes] = None) -> BytesBuffer: + # return self.loop.run_until_complete(self._async_execute(reqtype, args)) + + def read_request(self, command_id: Union[int, VS, VSFR]) -> BytesBuffer: + return self.loop.run_until_complete(self.async_read_request(command_id)) + + def write_request(self, command_id: Union[int, VSFR], data: Optional[bytes] = None) -> None: + return self.loop.run_until_complete(self.async_write_request(command_id, data)) + + def batch_read_vsfrs(self, vsfr_ids: List[VSFR]) -> List[int]: + return self.loop.run_until_complete(self.async_batch_read_vsfrs(vsfr_ids)) + + def status(self) -> str: + return self.loop.run_until_complete(self.async_status()) + + def set_local_time(self, dt: datetime.datetime) -> None: + return self.loop.run_until_complete(self.async_set_local_time(dt)) + + def fw_signature(self) -> str: + return self.loop.run_until_complete(self.async_fw_signature()) + + def fw_version(self) -> tuple[tuple[int, int, str], tuple[int, int, str]]: + return self.loop.run_until_complete(self.async_fw_version()) + + def hw_serial_number(self) -> str: + return self.loop.run_until_complete(self.async_hw_serial_number()) + + def configuration(self) -> str: + return self.loop.run_until_complete(self.async_configuration()) + + def text_message(self) -> str: + return self.loop.run_until_complete(self.async_text_message()) + + def serial_number(self) -> str: + return self.loop.run_until_complete(self.async_serial_number()) + + def commands(self) -> str: + return self.loop.run_until_complete(self.async_commands()) + + def device_time(self, v: int) -> None: + return self.loop.run_until_complete(self.async_device_time(v)) + + def data_buf(self) -> List[Union[DoseRateDB, RareData, RealTimeData, RawData, Event]]: + return self.loop.run_until_complete(self.async_data_buf()) + + def spectrum(self) -> Spectrum: + return self.loop.run_until_complete(self.async_spectrum()) + + def spectrum_accum(self) -> Spectrum: + return self.loop.run_until_complete(self.async_spectrum_accum()) + + def dose_reset(self) -> None: + return self.loop.run_until_complete(self.async_dose_reset()) + + def spectrum_reset(self) -> None: + return self.loop.run_until_complete(self.async_spectrum_reset()) + + def energy_calib(self) -> List[float]: + return self.loop.run_until_complete(self.async_energy_calib()) + + def set_energy_calib(self, coef: List[float]) -> None: + return self.loop.run_until_complete(self.async_set_energy_calib(coef)) + + def set_language(self, lang='en') -> None: + return self.loop.run_until_complete(self.async_set_language(lang)) + + def set_device_on(self, on: bool): + return self.loop.run_until_complete(self.async_set_device_on(on)) + + def set_sound_on(self, on: bool) -> None: + return self.loop.run_until_complete(self.async_set_sound_on(on)) + + def set_vibro_on(self, on: bool) -> None: + return self.loop.run_until_complete(self.async_set_vibro_on(on)) + + def set_sound_ctrl(self, ctrls: List[CTRL]) -> None: + return self.loop.run_until_complete(self.async_set_sound_ctrl(ctrls)) + + def set_display_off_time(self, seconds: int) -> None: + return self.loop.run_until_complete(self.async_set_display_off_time(seconds)) + + def set_display_brightness(self, brightness: int) -> None: + return self.loop.run_until_complete(self.async_set_display_brightness(brightness)) + + def set_display_direction(self, direction: DisplayDirection) -> None: + return self.loop.run_until_complete(self.async_set_display_direction(direction)) + + def set_vibro_ctrl(self, ctrls: List[CTRL]) -> None: + return self.loop.run_until_complete(self.async_set_vibro_ctrl(ctrls)) + + async def _async_execute(self, reqtype: bytes, args: Optional[bytes] = None) -> BytesBuffer: assert len(reqtype) == 2 req_seq_no = 0x80 + self._seq self._seq = (self._seq + 1) % 32 @@ -65,54 +231,56 @@ def execute(self, reqtype: bytes, args: Optional[bytes] = None) -> BytesBuffer: request = req_header + (args or b'') full_request = struct.pack(' BytesBuffer: - r = self.execute(b'\x26\x08', struct.pack(' BytesBuffer: + r = await self._async_execute(b'\x26\x08', struct.pack(' None: - r = self.execute(b'\x25\x08', struct.pack(' None: + r = await self._async_execute(b'\x25\x08', struct.pack(' List[int]: + async def async_batch_read_vsfrs(self, vsfr_ids: List[VSFR]) -> List[int]: assert len(vsfr_ids) - r = self.execute(b'\x2a\x08', b''.join(struct.pack(' str: - r = self.execute(b'\x05\x00') + async def async_status(self) -> str: + r = await self._async_execute(b'\x05\x00') flags = r.unpack(' None: + async def async_set_local_time(self, dt: datetime.datetime) -> None: d = struct.pack(' str: - r = self.execute(b'\x01\x01') + async def async_fw_signature(self) -> str: + r = await self._async_execute(b'\x01\x01') signature = r.unpack(' tuple[tuple[int, int, str], tuple[int, int, str]]: - r = self.execute(b'\x0a\x00') + async def async_fw_version(self) -> tuple[tuple[int, int, str], tuple[int, int, str]]: + r = await self._async_execute(b'\x0a\x00') boot_minor, boot_major = r.unpack(' tuple[tuple[int, int, str], tuple[int, int, str]]: assert r.size() == 0 return ((boot_major, boot_minor, boot_date), (target_major, target_minor, target_date.strip('\x00'))) - def hw_serial_number(self) -> str: - r = self.execute(b'\x0b\x00') + async def async_hw_serial_number(self) -> str: + r = await self._async_execute(b'\x0b\x00') serial_len = r.unpack(' str: - r = self.read_request(VS.CONFIGURATION) + async def async_configuration(self) -> str: + r = await self.async_read_request(VS.CONFIGURATION) return r.data().decode('cp1251') - def text_message(self) -> str: - r = self.read_request(VS.TEXT_MESSAGE) + async def async_text_message(self) -> str: + r = await self.async_read_request(VS.TEXT_MESSAGE) return r.data().decode('ascii') - def serial_number(self) -> str: - r = self.read_request(8) + async def async_serial_number(self) -> str: + r = await self.async_read_request(8) return r.data().decode('ascii') - def commands(self) -> str: - br = self.read_request(257) + async def async_commands(self) -> str: + br = await self.async_read_request(257) return br.data().decode('ascii') # called with 0 after init! - def device_time(self, v: int) -> None: - self.write_request(VSFR.DEVICE_TIME, struct.pack(' None: + await self.async_write_request(VSFR.DEVICE_TIME, struct.pack(' List[Union[DoseRateDB, RareData, RealTimeData, RawData, Event]]: - r = self.read_request(VS.DATA_BUF) + async def async_data_buf(self) -> List[Union[DoseRateDB, RareData, RealTimeData, RawData, Event]]: + r = await self.async_read_request(VS.DATA_BUF) return decode_VS_DATA_BUF(r, self._base_time) - def spectrum(self) -> Spectrum: - r = self.read_request(VS.SPECTRUM) + async def async_spectrum(self) -> Spectrum: + r = await self.async_read_request(VS.SPECTRUM) return decode_RC_VS_SPECTRUM(r, self._spectrum_format_version) - def spectrum_accum(self) -> Spectrum: - r = self.read_request(VS.SPEC_ACCUM) + async def async_spectrum_accum(self) -> Spectrum: + r = await self.async_read_request(VS.SPEC_ACCUM) return decode_RC_VS_SPECTRUM(r, self._spectrum_format_version) - def dose_reset(self) -> None: - self.write_request(VSFR.DOSE_RESET) + async def async_dose_reset(self) -> None: + await self.async_write_request(VSFR.DOSE_RESET) - def spectrum_reset(self) -> None: - r = self.execute(b'\x27\x08', struct.pack(' None: + r = await self._async_execute(b'\x27\x08', struct.pack(' List[float]: - r = self.read_request(VS.ENERGY_CALIB) + async def async_energy_calib(self) -> List[float]: + r = await self.async_read_request(VS.ENERGY_CALIB) return list(r.unpack(' None: + async def async_set_energy_calib(self, coef: List[float]) -> None: assert len(coef) == 3 pc = struct.pack(' None: + async def async_set_language(self, lang='en') -> None: assert lang in {'ru', 'en'}, 'unsupported lang value - use "ru" or "en"' - self.write_request(VSFR.DEVICE_LANG, struct.pack(' None: - self.write_request(VSFR.SOUND_ON, struct.pack(' None: + await self.async_write_request(VSFR.SOUND_ON, struct.pack(' None: - self.write_request(VSFR.SOUND_ON, struct.pack(' None: + await self.async_write_request(VSFR.SOUND_ON, struct.pack(' None: + async def async_set_sound_ctrl(self, ctrls: List[CTRL]) -> None: flags = 0 + for c in ctrls: flags |= int(c) - self.write_request(VSFR.SOUND_CTRL, struct.pack(' None: + async def async_set_display_off_time(self, seconds: int) -> None: assert seconds in {5, 10, 15, 30} v = 3 if seconds == 30 else (seconds // 5) - 1 - self.write_request(VSFR.DISP_OFF_TIME, struct.pack(' None: + async def async_set_display_brightness(self, brightness: int) -> None: assert 0 <= brightness and brightness <= 9 - self.write_request(VSFR.DISP_BRT, struct.pack(' None: + async def async_set_display_direction(self, direction: DisplayDirection) -> None: assert isinstance(direction, DisplayDirection) - self.write_request(VSFR.DISP_DIR, struct.pack(' None: + async def async_set_vibro_ctrl(self, ctrls: List[CTRL]) -> None: flags = 0 for c in ctrls: assert c != CTRL.CLICKS, 'CTRL.CLICKS not supported for vibro' flags |= int(c) - self.write_request(VSFR.VIBRO_CTRL, struct.pack(' None: + if self.bluetooth_mac: + if len(self.bluetooth_mac) != 17: + Logger.warning('The provided MAC address does not seem to be valid, but we will try anyway') + + self.client = BleakClient(self.bluetooth_mac) + elif self.bluetooth_serial: + if len(self.bluetooth_serial) < 6: + Logger.warning('To improve reliability, try to provide at least 6 digits of your Radiacode serial number') + + bt_devices = await self._async_scan() + + if len(bt_devices) == 0: + raise DeviceNotFound( + 'No Radiacodes found. Check that bluetooth is enabled and that the Radiacode is not connected to another device.' + ) + + ble, _ = (None, None) + + # Find the requested device in the list + for b, a in bt_devices: + if self.bluetooth_serial in str(a.local_name): + ble = b + break + + if ble is None: + raise DeviceNotFound(f'No matching serial found while scanning. We were looking for: {self.bluetooth_serial}') + + self.client = BleakClient(ble) + elif self.bluetooth_uuid: + if len(self.bluetooth_uuid) != 36: + Logger.warning('The provided UUID does not seem to be valid, but we will try anyway') + + self.client = BleakClient(self.bluetooth_uuid) + else: + raise DeviceNotFound('No connection parameters specified.') + + # Attempt connection + await self.client.connect() + + if self.client.is_connected: + Logger.notify(f'Connected to Radiacode via bluetooth to: {self.client.address}') + else: + raise DeviceNotFound('Cannot connect to the requested Radiacode') + + # Start notifications + self.notiffd = self.client.services.get_characteristic(RADIACODE_NOTIFYFD_UUID) + await self.client.start_notify(self.notiffd, self.handleNotification) + + self.writefd = self.client.services.get_characteristic(RADIACODE_WRITEFD_UUID) + + Logger.notify('Notifications started') - try: - self.p = Peripheral(mac) - except BTLEDisconnectError as ex: - raise DeviceNotFound('Device not found or bluetooth adapter is not powered on') from ex + def _scan(self) -> List[Tuple[BLEDevice, AdvertisementData]]: + return self.loop.run_until_complete(self._async_scan()) - self.p.withDelegate(self) + async def _async_scan(self) -> List[Tuple[BLEDevice, AdvertisementData]]: + """Returns a list of Tuples of valid Radiacodes""" + radiacodes = [] - service = self.p.getServiceByUUID('e63215e5-7003-49d8-96b0-b024798fb901') - self.write_fd = service.getCharacteristics('e63215e6-7003-49d8-96b0-b024798fb901')[0].getHandle() - notify_fd = service.getCharacteristics('e63215e7-7003-49d8-96b0-b024798fb901')[0].getHandle() - self.p.writeCharacteristic(notify_fd + 1, b'\x01\x00') + Logger.info('Scan started...') + devices = await BleakScanner.discover(return_adv=True, cb=dict(use_bdaddr=False)) - def handleNotification(self, chandle, data): - if self._resp_size == 0: - self._resp_size = 4 + struct.unpack('= 0 - if self._resp_size == 0: - self._response = self._resp_buffer - self._resp_buffer = b'' - - def execute(self, req) -> BytesBuffer: - for pos in range(0, len(req), 18): - rp = req[pos : min(pos + 18, len(req))] - self.p.writeCharacteristic(self.write_fd, rp) - - while self._response is None: - self.p.waitForNotifications(2.0) - - br = BytesBuffer(self._response) - self._response = None - return br + Logger.notify(f'Active service found for device ID: {adv.local_name} - UUID: {ble.address}') + + bt_devices.append(r) + + return bt_devices + + def handleNotification(self, characteristic, data) -> None: + if self._resp_size == 0: + self._resp_size = 4 + struct.unpack('= 0 + + if self._resp_size == 0: + self._response = self._resp_buffer + self._resp_buffer = b'' + + # self._response_event.set() + # Logger.notify(f'Notification: {characteristic.description}: {self._resp_buffer}') + + async def execute(self, req) -> BytesBuffer: + if self.client is None or self.client.is_connected is False: + await self.client.connect() + + if self.client.is_connected is False: + raise DeviceNotFound('Connection to the device not active') + + # Request data + self._response = [] + await self.client.write_gatt_char(self.writefd, req, response=False) + await asyncio.sleep(1.5) + + # If we knew how much data to expect from every request, we could monitor + # for that in handleNotification(), rather than sleeping + # await self._response_event.wait() + # await self.client.stop_notify(notif) + + br = BytesBuffer(self._response) + self._response = None + return br diff --git a/radiacode/transports/usb.py b/radiacode/transports/usb.py index dfac09e..c05d727 100644 --- a/radiacode/transports/usb.py +++ b/radiacode/transports/usb.py @@ -27,26 +27,31 @@ def __init__(self, serial_number=None, timeout_ms=3000): # usb.core.find(..., serial_number=None) will attempt to match against a value of None, # rather than ignoring it as a match condition. self._device = usb.core.find(idVendor=_vid, idProduct=_pid) + self._timeout_ms = timeout_ms + if self._device is None: raise DeviceNotFound + while True: try: self._device.read(0x81, 256, timeout=100) except usb.core.USBTimeoutError: break - def execute(self, request: bytes) -> BytesBuffer: + async def execute(self, request: bytes) -> BytesBuffer: self._device.write(0x1, request) trials = 0 max_trials = 3 + while trials < max_trials: # repeat until non-zero lenght data received data = self._device.read(0x81, 256, timeout=self._timeout_ms).tobytes() if len(data) != 0: break else: trials += 1 + if trials >= max_trials: raise MultipleUSBReadFailure(str(trials) + ' USB Read Failures in sequence') diff --git a/tests/test_radiacode.py b/tests/test_radiacode.py new file mode 100644 index 0000000..b698edc --- /dev/null +++ b/tests/test_radiacode.py @@ -0,0 +1,93 @@ +import unittest +import datetime +from radiacode.radiacode import RadiaCode +from radiacode.types import DoseRateDB, Event, RareData, RawData, RealTimeData, Spectrum + +class TestRadiaCodeIntegration(unittest.TestCase): + """ + This test class requires a real and available Radiacode + """ + @classmethod + def setUpClass(cls): + # Connect to the device only once + cls.rc = RadiaCode(bluetooth_serial='RadiaCode-10') + + def test_serial_number_integration(self): + serial_number = self.rc.serial_number() + + self.assertIsInstance(serial_number, str) + self.assertIn('RC-10', serial_number) + + def test_fw_version_integration(self): + fw_version = self.rc.fw_version() + + self.assertIsInstance(fw_version, tuple) + self.assertEqual(len(fw_version), 2) + + # tuple[tuple[int, int, str], tuple[int, int, str]]: + for ver in fw_version: + self.assertIsInstance(ver, tuple) + self.assertEqual(len(ver), 3) + self.assertIsInstance(ver[0], int) + self.assertIsInstance(ver[1], int) + self.assertIsInstance(ver[2], str) + + def test_spectrum_integration(self): + spectrum = self.rc.spectrum() + + self.assertIsInstance(spectrum, Spectrum) + self.assertIsInstance(spectrum.duration, datetime.timedelta) + self.assertIsInstance(spectrum.a0, float) + self.assertIsInstance(spectrum.a1, float) + self.assertIsInstance(spectrum.a2, float) + self.assertIsInstance(spectrum.counts, list) + self.assertTrue(all(isinstance(count, int) for count in spectrum.counts)) + + def test_data_buf_integration(self): + data_buf = self.rc.data_buf() + + self.assertIsInstance(data_buf, list) + for item in data_buf: + self.assertTrue(isinstance(item, (DoseRateDB, RareData, RealTimeData, RawData, Event))) + self.assertIsInstance(item.dt, datetime.datetime) + + def test_spectrum_accum_integration(self): + spectrum_accum = self.rc.spectrum_accum() + + self.assertIsInstance(spectrum_accum, Spectrum) + self.assertIsInstance(spectrum_accum.duration, datetime.timedelta) + self.assertIsInstance(spectrum_accum.a0, float) + self.assertIsInstance(spectrum_accum.a1, float) + self.assertIsInstance(spectrum_accum.a2, float) + self.assertIsInstance(spectrum_accum.counts, list) + self.assertTrue(all(isinstance(count, int) for count in spectrum_accum.counts)) + + def test_configuration_integration(self): + configuration = self.rc.configuration() + + self.assertIsInstance(configuration, str) + self.assertIn('DeviceParams', configuration) + self.assertIn('CHN_ChargeLevel', configuration) + + def test_hw_serial_number_integration(self): + hw_serial_number = self.rc.hw_serial_number() + + # Format is: XXXXXXXX-XXXXXXXX-XXXXXXXX (hex) + self.assertIsInstance(hw_serial_number, str) + self.assertIn('-', hw_serial_number) + + def test_status_integration(self): + status = self.rc.status() + + self.assertIsInstance(status, str) + self.assertIn('flags: ', status) + + def test_commands_integration(self): + commands = self.rc.commands() + + self.assertIsInstance(commands, str) + self.assertIn('VSFR_DEVICE_CTRL', commands) + self.assertIn('VSFR_SYS_MCU_TEMP', commands) + +if __name__ == '__main__': + unittest.main()