From 3b2c7569b9611695d1be64e93544f17a0c34a4e2 Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Sun, 15 Sep 2024 21:36:22 +0200 Subject: [PATCH] bump simpleble and start adding syncroni devices Signed-off-by: Andrey Parfenov --- .../brainflow/board_controller_library.cs | 4 +- .../src/main/java/brainflow/BoardIds.java | 4 +- julia_package/brainflow/src/board_shim.jl | 2 + matlab_package/brainflow/BoardIds.m | 2 + nodejs_package/brainflow/brainflow.types.ts | 4 +- python_package/brainflow/board_shim.py | 2 + src/board_controller/board_controller.cpp | 7 + src/board_controller/brainflow_boards.cpp | 25 +- src/board_controller/build.cmake | 2 + .../synchroni/inc/synchroni_board.h | 36 + .../synchroni/synchroni_board.cpp | 380 +++++ src/utils/inc/brainflow_constants.h | 4 +- third_party/SimpleBLE/.github/FUNDING.yml | 13 + .../.github/workflows/ci_build_examples.yml | 49 +- .../.github/workflows/ci_build_lib.yml | 115 -- .../.github/workflows/ci_cpp_release.yml | 205 +++ .../.github/workflows/ci_cpp_release_test.yml | 163 ++ .../SimpleBLE/.github/workflows/ci_docs.yml | 10 +- .../SimpleBLE/.github/workflows/ci_lint.yml | 6 +- .../.github/workflows/ci_py_release.yml | 133 ++ .../.github/workflows/ci_py_release_test.yml | 98 ++ .../.github/workflows/ci_release.yml | 57 - .../.github/workflows/ci_release_test.yml | 58 - .../.github/workflows/ci_rust_build.yml | 74 + .../SimpleBLE/.github/workflows/ci_test.yml | 47 +- third_party/SimpleBLE/.gitignore | 6 +- third_party/SimpleBLE/.readthedocs.yaml | 2 +- third_party/SimpleBLE/CONTRIBUTING.md | 45 + third_party/SimpleBLE/Cargo.lock | 183 +++ third_party/SimpleBLE/Cargo.toml | 31 + third_party/SimpleBLE/LICENSE.md | 446 +++++- third_party/SimpleBLE/MANIFEST.in | 178 +++ third_party/SimpleBLE/README.rst | 122 +- third_party/SimpleBLE/VERSION | 2 +- .../SimpleBLE/cmake/find/FindDBus1.cmake | 6 - .../SimpleBLE/cmake/find/Findfmt.cmake | 2 +- third_party/SimpleBLE/docs/Doxyfile | 2 +- third_party/SimpleBLE/docs/changelog.rst | 104 +- third_party/SimpleBLE/docs/conf.py | 5 +- third_party/SimpleBLE/docs/extras.rst | 31 +- third_party/SimpleBLE/docs/index.rst | 9 +- third_party/SimpleBLE/docs/licensing_faq.rst | 1387 +++++++++++++++++ third_party/SimpleBLE/docs/overview.rst | 80 +- third_party/SimpleBLE/docs/requirements.txt | 1 + third_party/SimpleBLE/docs/simpleble/api.rst | 11 + third_party/SimpleBLE/docs/simpleble/faq.rst | 7 + .../SimpleBLE/docs/simpleble/tutorial.rst | 8 +- .../SimpleBLE/docs/simpleble/usage.rst | 181 ++- .../SimpleBLE/docs/simplebluez/usage.rst | 114 +- .../SimpleBLE/docs/simpledbus/usage.rst | 114 +- .../SimpleBLE/docs/simpledroidble/usage.rst | 47 + .../examples/simpleble-android/.gitignore | 15 + .../examples/simpleble-android/app/.gitignore | 1 + .../simpleble-android/app/build.gradle.kts | 60 + .../simpleble-android/app/proguard-rules.pro | 21 + .../app/src/main/AndroidManifest.xml | 29 + .../android/SimpleBleAndroidExample.kt | 17 + .../android/activities/MainActivity.kt | 136 ++ .../android/viewmodels/BluetoothViewModel.kt | 16 + .../examples/android/views/ConnectContent.kt | 230 +++ .../android/views/ListAdaptersContent.kt | 88 ++ .../examples/android/views/NotifyContent.kt | 254 +++ .../examples/android/views/ReadContent.kt | 197 +++ .../examples/android/views/ScanContent.kt | 154 ++ .../drawable/ic_dashboard_black_24dp.xml | 9 + .../drawable/ic_home_black_24dp.xml | 9 + .../drawable/ic_launcher_background.xml | 170 ++ .../drawable/ic_launcher_foreground.xml | 30 + .../drawable/ic_notifications_black_24dp.xml | 9 + .../main/res_legacy/layout/activity_main.xml | 33 + .../res_legacy/layout/fragment_dashboard.xml | 22 + .../main/res_legacy/layout/fragment_home.xml | 22 + .../layout/fragment_notifications.xml | 22 + .../main/res_legacy/menu/bottom_nav_menu.xml | 19 + .../res_legacy/mipmap-anydpi/ic_launcher.xml | 6 + .../mipmap-anydpi/ic_launcher_round.xml | 6 + .../res_legacy/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../res_legacy/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../res_legacy/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../res_legacy/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../navigation/mobile_navigation.xml | 25 + .../main/res_legacy/values-night/themes.xml | 16 + .../app/src/main/res_legacy/values/colors.xml | 10 + .../app/src/main/res_legacy/values/dimens.xml | 5 + .../src/main/res_legacy/values/strings.xml | 6 + .../app/src/main/res_legacy/values/themes.xml | 16 + .../src/main/res_legacy/xml/backup_rules.xml | 13 + .../res_legacy/xml/data_extraction_rules.xml | 19 + .../simpleble-android/build.gradle.kts | 5 + .../simpleble-android/gradle.properties | 23 + .../gradle/libs.versions.toml | 29 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + .../examples/simpleble-android/gradlew | 185 +++ .../examples/simpleble-android/gradlew.bat | 89 ++ .../simpleble-android/settings.gradle.kts | 30 + .../examples/simpleble/CMakeLists.txt | 14 +- .../simpleble/c/{connect => }/connect.c | 0 .../simpleble/c/connect/CMakeLists.txt | 7 - .../simpleble/c/{notify => }/notify.c | 8 +- .../simpleble/c/notify/CMakeLists.txt | 7 - .../examples/simpleble/c/{scan => }/scan.c | 54 + .../examples/simpleble/c/scan/CMakeLists.txt | 7 - .../examples/simpleble/cpp/common/utils.cpp | 7 - .../examples/simpleble/cpp/common/utils.hpp | 4 - .../simpleble/cpp/connect/connect.cpp | 9 +- .../cpp/connect_safe/connect_safe.cpp | 2 +- .../cpp/list_adapters/list_adapters.cpp | 4 +- .../cpp/list_adapters_safe/CMakeLists.txt | 11 + .../list_adapters_safe/list_adapters_safe.cpp | 33 + .../simpleble/cpp/multiconnect/CMakeLists.txt | 12 + .../cpp/multiconnect/multiconnect.cpp | 60 + .../examples/simpleble/cpp/notify/notify.cpp | 10 +- .../cpp/notify_multi/notify_multi.cpp | 3 +- .../examples/simpleble/cpp/read/read.cpp | 7 +- .../examples/simpleble/cpp/scan/scan.cpp | 14 +- .../examples/simpleble/cpp/write/write.cpp | 8 +- .../examples/simplebluez/scan/scan.cpp | 2 + .../SimpleBLE/examples/simplepyble/connect.py | 3 + .../examples/simplepyble/list_adapters.py | 2 + .../SimpleBLE/examples/simplepyble/scan.py | 7 + .../SimpleBLE/examples/simplersble/Cargo.lock | 190 +++ .../SimpleBLE/examples/simplersble/Cargo.toml | 19 + .../external/include/external/kvn_bytearray.h | 200 +++ .../{simplepyble => }/pyproject.toml | 6 +- .../SimpleBLE/{simplepyble => }/setup.py | 81 +- .../SimpleBLE/simpleble/CMakeLists.txt | 183 ++- .../simpleble/include/simpleble/Adapter.h | 4 +- .../simpleble/include/simpleble/AdapterSafe.h | 4 +- .../include/simpleble/Characteristic.h | 4 +- .../simpleble/include/simpleble/Descriptor.h | 4 +- .../simpleble/include/simpleble/Exceptions.h | 24 +- .../simpleble/include/simpleble/Logging.h | 4 +- .../simpleble/include/simpleble/Peripheral.h | 4 +- .../include/simpleble/PeripheralSafe.h | 4 +- .../simpleble/include/simpleble/Service.h | 4 +- .../simpleble/include/simpleble/Types.h | 15 +- .../simpleble/include/simpleble/Utils.h | 6 +- .../simpleble/include/simpleble_c/adapter.h | 2 - .../simpleble/include/simpleble_c/logging.h | 2 - .../include/simpleble_c/peripheral.h | 22 +- .../simpleble/include/simpleble_c/simpleble.h | 1 - .../simpleble/include/simpleble_c/utils.h | 2 - third_party/SimpleBLE/simpleble/src/Utils.cpp | 2 +- .../src/backends/android/AdapterBase.cpp | 167 ++ .../src/backends/android/AdapterBase.h | 86 + .../src/backends/android/PeripheralBase.cpp | 312 ++++ .../src/backends/android/PeripheralBase.h | 74 + .../android/android/BluetoothDevice.cpp | 46 + .../android/android/BluetoothDevice.h | 33 + .../android/android/BluetoothGatt.cpp | 146 ++ .../backends/android/android/BluetoothGatt.h | 64 + .../android/BluetoothGattCharacteristic.cpp | 144 ++ .../android/BluetoothGattCharacteristic.h | 57 + .../android/BluetoothGattDescriptor.cpp | 70 + .../android/android/BluetoothGattDescriptor.h | 39 + .../android/android/BluetoothGattService.cpp | 122 ++ .../android/android/BluetoothGattService.h | 43 + .../backends/android/android/ScanResult.cpp | 29 + .../src/backends/android/android/ScanResult.h | 25 + .../src/backends/android/android/UUID.cpp | 34 + .../src/backends/android/android/UUID.h | 25 + .../android/bridge/BluetoothGattCallback.cpp | 410 +++++ .../android/bridge/BluetoothGattCallback.h | 95 ++ .../backends/android/bridge/ScanCallback.cpp | 98 ++ .../backends/android/bridge/ScanCallback.h | 41 + .../src/backends/android/jni/Common.hpp | 321 ++++ .../src/backends/android/jni/GlobalRef.hpp | 49 + .../src/backends/android/jni/Types.h | 28 + .../simpleble/src/backends/android/jni/VM.hpp | 46 + .../android/simpleble-bridge/.gitignore | 15 + .../android/simpleble-bridge/build.gradle.kts | 17 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 63375 bytes .../gradle/wrapper/gradle-wrapper.properties | 8 + .../backends/android/simpleble-bridge/gradlew | 248 +++ .../android/simpleble-bridge/gradlew.bat | 92 ++ .../simpleble-bridge/settings.gradle.kts | 22 + .../src/main/AndroidManifest.xml | 9 + .../android/bridge/BluetoothGattCallback.java | 141 ++ .../android/bridge/ScanCallback.java | 36 + .../src/backends/linux/AdapterBase.cpp | 3 + .../src/backends/linux/PeripheralBase.cpp | 14 +- .../src/backends/macos/PeripheralBase.mm | 12 +- .../src/backends/macos/PeripheralBaseMacOS.mm | 18 +- .../simpleble/src/backends/macos/Utils.h | 1 + .../simpleble/src/backends/macos/Utils.mm | 10 + .../src/backends/plain/PeripheralBase.cpp | 51 +- .../src/backends/plain/PeripheralBase.h | 8 + .../src/backends/windows/AdapterBase.cpp | 6 +- .../src/backends/windows/PeripheralBase.h | 2 + .../simpleble/src/external/TaskRunner.hpp | 99 ++ .../simpleble/src/external/ThreadRunner.h | 54 + .../SimpleBLE/simpleble/src_c/peripheral.cpp | 12 +- .../SimpleBLE/simpleble/src_c/utils.cpp | 2 +- .../simpleble/test/src/test_bytearray.cpp | 246 +++ .../SimpleBLE/simplebluez/CMakeLists.txt | 19 +- .../simplebluez/include/simplebluez/Device.h | 5 +- .../simplebluez/include/simplebluez/Types.h | 4 +- .../include/simplebluez/interfaces/Device1.h | 11 +- .../SimpleBLE/simplebluez/src/Device.cpp | 22 +- .../simplebluez/src/interfaces/Device1.cpp | 19 +- .../SimpleBLE/simpledbus/CMakeLists.txt | 8 + .../include/simpledbus/advanced/Proxy.h | 14 + .../include/simpledbus/base/Holder.h | 39 +- .../simpledbus/include/simpledbus/base/Path.h | 1 + .../SimpleBLE/simpledbus/src/base/Holder.cpp | 195 ++- .../SimpleBLE/simpledbus/src/base/Path.cpp | 5 + .../SimpleBLE/simpledroidble/.gitignore | 15 + .../SimpleBLE/simpledroidble/build.gradle.kts | 5 + .../simpledroidble/gradle.properties | 23 + .../simpledroidble/gradle/libs.versions.toml | 15 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + third_party/SimpleBLE/simpledroidble/gradlew | 185 +++ .../SimpleBLE/simpledroidble/gradlew.bat | 89 ++ .../simpledroidble/settings.gradle.kts | 29 + .../simpledroidble/simpledroidble/.gitignore | 1 + .../simpledroidble/build.gradle.kts | 52 + .../simpledroidble/consumer-rules.pro | 0 .../simpledroidble/proguard-rules.pro | 21 + .../src/main/AndroidManifest.xml | 8 + .../src/main/cpp/CMakeLists.txt | 48 + .../src/main/cpp/ThreadRunner.h | 91 ++ .../src/main/cpp/android_utils.cpp | 64 + .../src/main/cpp/android_utils.h | 18 + .../src/main/cpp/simpleble_android.cpp | 784 ++++++++++ .../java/org/simpleble/android/Adapter.kt | 162 ++ .../org/simpleble/android/BluetoothAddress.kt | 7 + .../simpleble/android/BluetoothAddressType.kt | 21 + .../org/simpleble/android/BluetoothUUID.kt | 7 + .../org/simpleble/android/Characteristic.kt | 11 + .../java/org/simpleble/android/Descriptor.kt | 3 + .../java/org/simpleble/android/Peripheral.kt | 284 ++++ .../java/org/simpleble/android/Service.kt | 6 + .../org/simpleble/android/SimpleDroidBle.kt | 101 ++ .../SimpleBLE/simplepyble/CMakeLists.txt | 15 +- third_party/SimpleBLE/simplepyble/README.rst | 23 +- .../cmake_build_extension/__init__.py | 49 - .../cmake_build_extension/build_ext_option.py | 55 - .../cmake_build_extension/build_extension.py | 281 ---- .../cmake_build_extension/cmake_extension.py | 58 - .../cmake_build_extension/sdist_command.py | 145 -- .../SimpleBLE/simplepyble/requirements.txt | 9 +- .../SimpleBLE/simplepyble/src/main.cpp | 8 +- .../simplepyble/src/simplepyble/__init__.py | 1 + .../simplepyble/src/wrap_characteristic.cpp | 32 +- .../simplepyble/src/wrap_peripheral.cpp | 56 +- .../simplepyble/src/wrap_service.cpp | 6 + .../SimpleBLE/simplepyble/src/wrap_types.cpp | 21 + third_party/SimpleBLE/simplersble/README.md | 53 + third_party/SimpleBLE/simplersble/build.rs | 62 + .../simplersble/src/bindings/Bindings.cpp | 328 ++++ .../simplersble/src/bindings/Bindings.hpp | 177 +++ third_party/SimpleBLE/simplersble/src/lib.rs | 854 ++++++++++ third_party/SimpleBLE/utils/build_android.sh | 104 ++ third_party/SimpleBLE/utils/build_docs.sh | 4 + third_party/SimpleBLE/utils/build_lib.sh | 12 +- .../SimpleBLE/utils/clean_workflows.py | 100 ++ third_party/SimpleBLE/utils/format.sh | 75 + 265 files changed, 15011 insertions(+), 1403 deletions(-) create mode 100644 src/board_controller/synchroni/inc/synchroni_board.h create mode 100644 src/board_controller/synchroni/synchroni_board.cpp create mode 100644 third_party/SimpleBLE/.github/FUNDING.yml delete mode 100644 third_party/SimpleBLE/.github/workflows/ci_build_lib.yml create mode 100644 third_party/SimpleBLE/.github/workflows/ci_cpp_release.yml create mode 100644 third_party/SimpleBLE/.github/workflows/ci_cpp_release_test.yml create mode 100644 third_party/SimpleBLE/.github/workflows/ci_py_release.yml create mode 100644 third_party/SimpleBLE/.github/workflows/ci_py_release_test.yml delete mode 100644 third_party/SimpleBLE/.github/workflows/ci_release.yml delete mode 100644 third_party/SimpleBLE/.github/workflows/ci_release_test.yml create mode 100644 third_party/SimpleBLE/.github/workflows/ci_rust_build.yml create mode 100644 third_party/SimpleBLE/CONTRIBUTING.md create mode 100644 third_party/SimpleBLE/Cargo.lock create mode 100644 third_party/SimpleBLE/Cargo.toml create mode 100644 third_party/SimpleBLE/MANIFEST.in create mode 100644 third_party/SimpleBLE/docs/licensing_faq.rst create mode 100644 third_party/SimpleBLE/docs/simpledroidble/usage.rst create mode 100644 third_party/SimpleBLE/examples/simpleble-android/.gitignore create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/.gitignore create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/build.gradle.kts create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/proguard-rules.pro create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/AndroidManifest.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/SimpleBleAndroidExample.kt create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/activities/MainActivity.kt create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/viewmodels/BluetoothViewModel.kt create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/ConnectContent.kt create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/ListAdaptersContent.kt create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/NotifyContent.kt create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/ReadContent.kt create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/ScanContent.kt create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_dashboard_black_24dp.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_home_black_24dp.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_launcher_background.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_launcher_foreground.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_notifications_black_24dp.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/layout/activity_main.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/layout/fragment_dashboard.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/layout/fragment_home.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/layout/fragment_notifications.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/menu/bottom_nav_menu.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-anydpi/ic_launcher.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-anydpi/ic_launcher_round.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-hdpi/ic_launcher.webp create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-hdpi/ic_launcher_round.webp create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-mdpi/ic_launcher.webp create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-mdpi/ic_launcher_round.webp create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-xhdpi/ic_launcher.webp create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-xxhdpi/ic_launcher.webp create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/navigation/mobile_navigation.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values-night/themes.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values/colors.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values/dimens.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values/strings.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values/themes.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/xml/backup_rules.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/xml/data_extraction_rules.xml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/build.gradle.kts create mode 100644 third_party/SimpleBLE/examples/simpleble-android/gradle.properties create mode 100644 third_party/SimpleBLE/examples/simpleble-android/gradle/libs.versions.toml create mode 100644 third_party/SimpleBLE/examples/simpleble-android/gradle/wrapper/gradle-wrapper.jar create mode 100644 third_party/SimpleBLE/examples/simpleble-android/gradle/wrapper/gradle-wrapper.properties create mode 100644 third_party/SimpleBLE/examples/simpleble-android/gradlew create mode 100644 third_party/SimpleBLE/examples/simpleble-android/gradlew.bat create mode 100644 third_party/SimpleBLE/examples/simpleble-android/settings.gradle.kts rename third_party/SimpleBLE/examples/simpleble/c/{connect => }/connect.c (100%) delete mode 100644 third_party/SimpleBLE/examples/simpleble/c/connect/CMakeLists.txt rename third_party/SimpleBLE/examples/simpleble/c/{notify => }/notify.c (94%) delete mode 100644 third_party/SimpleBLE/examples/simpleble/c/notify/CMakeLists.txt rename third_party/SimpleBLE/examples/simpleble/c/{scan => }/scan.c (63%) delete mode 100644 third_party/SimpleBLE/examples/simpleble/c/scan/CMakeLists.txt create mode 100644 third_party/SimpleBLE/examples/simpleble/cpp/list_adapters_safe/CMakeLists.txt create mode 100644 third_party/SimpleBLE/examples/simpleble/cpp/list_adapters_safe/list_adapters_safe.cpp create mode 100644 third_party/SimpleBLE/examples/simpleble/cpp/multiconnect/CMakeLists.txt create mode 100644 third_party/SimpleBLE/examples/simpleble/cpp/multiconnect/multiconnect.cpp create mode 100644 third_party/SimpleBLE/examples/simplersble/Cargo.lock create mode 100644 third_party/SimpleBLE/examples/simplersble/Cargo.toml create mode 100644 third_party/SimpleBLE/external/include/external/kvn_bytearray.h rename third_party/SimpleBLE/{simplepyble => }/pyproject.toml (55%) rename third_party/SimpleBLE/{simplepyble => }/setup.py (54%) create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/AdapterBase.cpp create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/AdapterBase.h create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/PeripheralBase.cpp create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/PeripheralBase.h create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothDevice.cpp create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothDevice.h create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGatt.cpp create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGatt.h create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattCharacteristic.cpp create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattCharacteristic.h create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattDescriptor.cpp create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattDescriptor.h create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattService.cpp create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattService.h create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/android/ScanResult.cpp create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/android/ScanResult.h create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/android/UUID.cpp create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/android/UUID.h create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/bridge/BluetoothGattCallback.cpp create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/bridge/BluetoothGattCallback.h create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/bridge/ScanCallback.cpp create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/bridge/ScanCallback.h create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/jni/Common.hpp create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/jni/GlobalRef.hpp create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/jni/Types.h create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/jni/VM.hpp create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/.gitignore create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/build.gradle.kts create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/gradle/wrapper/gradle-wrapper.jar create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/gradle/wrapper/gradle-wrapper.properties create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/gradlew create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/gradlew.bat create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/settings.gradle.kts create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/src/main/AndroidManifest.xml create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/src/main/java/org/simpleble/android/bridge/BluetoothGattCallback.java create mode 100644 third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/src/main/java/org/simpleble/android/bridge/ScanCallback.java create mode 100644 third_party/SimpleBLE/simpleble/src/external/TaskRunner.hpp create mode 100644 third_party/SimpleBLE/simpleble/src/external/ThreadRunner.h create mode 100644 third_party/SimpleBLE/simpleble/test/src/test_bytearray.cpp create mode 100644 third_party/SimpleBLE/simpledroidble/.gitignore create mode 100644 third_party/SimpleBLE/simpledroidble/build.gradle.kts create mode 100644 third_party/SimpleBLE/simpledroidble/gradle.properties create mode 100644 third_party/SimpleBLE/simpledroidble/gradle/libs.versions.toml create mode 100644 third_party/SimpleBLE/simpledroidble/gradle/wrapper/gradle-wrapper.jar create mode 100644 third_party/SimpleBLE/simpledroidble/gradle/wrapper/gradle-wrapper.properties create mode 100644 third_party/SimpleBLE/simpledroidble/gradlew create mode 100644 third_party/SimpleBLE/simpledroidble/gradlew.bat create mode 100644 third_party/SimpleBLE/simpledroidble/settings.gradle.kts create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/.gitignore create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/build.gradle.kts create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/consumer-rules.pro create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/proguard-rules.pro create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/AndroidManifest.xml create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/CMakeLists.txt create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/ThreadRunner.h create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/android_utils.cpp create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/android_utils.h create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/simpleble_android.cpp create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Adapter.kt create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/BluetoothAddress.kt create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/BluetoothAddressType.kt create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/BluetoothUUID.kt create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Characteristic.kt create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Descriptor.kt create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Peripheral.kt create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Service.kt create mode 100644 third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/SimpleDroidBle.kt delete mode 100644 third_party/SimpleBLE/simplepyble/cmake_build_extension/__init__.py delete mode 100644 third_party/SimpleBLE/simplepyble/cmake_build_extension/build_ext_option.py delete mode 100644 third_party/SimpleBLE/simplepyble/cmake_build_extension/build_extension.py delete mode 100644 third_party/SimpleBLE/simplepyble/cmake_build_extension/cmake_extension.py delete mode 100644 third_party/SimpleBLE/simplepyble/cmake_build_extension/sdist_command.py create mode 100644 third_party/SimpleBLE/simplepyble/src/simplepyble/__init__.py create mode 100644 third_party/SimpleBLE/simplepyble/src/wrap_types.cpp create mode 100644 third_party/SimpleBLE/simplersble/README.md create mode 100644 third_party/SimpleBLE/simplersble/build.rs create mode 100644 third_party/SimpleBLE/simplersble/src/bindings/Bindings.cpp create mode 100644 third_party/SimpleBLE/simplersble/src/bindings/Bindings.hpp create mode 100644 third_party/SimpleBLE/simplersble/src/lib.rs create mode 100644 third_party/SimpleBLE/utils/build_android.sh create mode 100644 third_party/SimpleBLE/utils/build_docs.sh create mode 100644 third_party/SimpleBLE/utils/clean_workflows.py create mode 100644 third_party/SimpleBLE/utils/format.sh diff --git a/csharp_package/brainflow/brainflow/board_controller_library.cs b/csharp_package/brainflow/brainflow/board_controller_library.cs index ca545b3aa..7ab6429a4 100644 --- a/csharp_package/brainflow/brainflow/board_controller_library.cs +++ b/csharp_package/brainflow/brainflow/board_controller_library.cs @@ -115,7 +115,9 @@ public enum BoardIds AAVAA_V3_BOARD = 53, EXPLORE_PLUS_8_CHAN_BOARD = 54, EXPLORE_PLUS_32_CHAN_BOARD = 55, - PIEEG_BOARD = 56 + PIEEG_BOARD = 56, + SYNCHRONI_3_CHANNELS_BOARD = 57, + SYNCHRONI_8_CHANNELS_BOARD = 58 }; diff --git a/java_package/brainflow/src/main/java/brainflow/BoardIds.java b/java_package/brainflow/src/main/java/brainflow/BoardIds.java index 6377c20af..2a21d3aca 100644 --- a/java_package/brainflow/src/main/java/brainflow/BoardIds.java +++ b/java_package/brainflow/src/main/java/brainflow/BoardIds.java @@ -65,7 +65,9 @@ public enum BoardIds AAVAA_V3_BOARD(53), EXPLORE_PLUS_8_CHAN_BOARD(54), EXPLORE_PLUS_32_CHAN_BOARD(55), - PIEEG_BOARD(56); + PIEEG_BOARD(56), + SYNCHRONI_3_CHANNELS_BOARD(57), + SYNCHRONI_8_CHANNELS_BOARD(58); private final int board_id; private static final Map bi_map = new HashMap (); diff --git a/julia_package/brainflow/src/board_shim.jl b/julia_package/brainflow/src/board_shim.jl index c21b94c83..d3dc4911b 100644 --- a/julia_package/brainflow/src/board_shim.jl +++ b/julia_package/brainflow/src/board_shim.jl @@ -61,6 +61,8 @@ export BrainFlowInputParams EXPLORE_PLUS_8_CHAN_BOARD = 54 EXPLORE_PLUS_32_CHAN_BOARD = 55 PIEEG_BOARD = 56 + SYNCHRONI_3_CHANNELS_BOARD = 57 + SYNCHRONI_8_CHANNELS_BOARD = 58 end diff --git a/matlab_package/brainflow/BoardIds.m b/matlab_package/brainflow/BoardIds.m index ceb601a86..69c655a6f 100644 --- a/matlab_package/brainflow/BoardIds.m +++ b/matlab_package/brainflow/BoardIds.m @@ -59,5 +59,7 @@ EXPLORE_PLUS_8_CHAN_BOARD(54) EXPLORE_PLUS_32_CHAN_BOARD(55) PIEEG_BOARD(56) + SYNCHRONI_3_CHANNELS_BOARD(57) + SYNCHRONI_8_CHANNELS_BOARD(58) end end \ No newline at end of file diff --git a/nodejs_package/brainflow/brainflow.types.ts b/nodejs_package/brainflow/brainflow.types.ts index c95ec7d1c..ed219d902 100644 --- a/nodejs_package/brainflow/brainflow.types.ts +++ b/nodejs_package/brainflow/brainflow.types.ts @@ -68,7 +68,9 @@ export enum BoardIds { ANT_NEURO_EE_511_BOARD = 51, EXPLORE_PLUS_8_CHAN_BOARD = 54, EXPLORE_PLUS_32_CHAN_BOARD = 55, - PIEEG_BOARD = 56 + PIEEG_BOARD = 56, + SYNCHRONI_3_CHANNELS_BOARD = 57, + SYNCHRONI_8_CHANNELS_BOARD = 58 } export enum IpProtocolTypes { diff --git a/python_package/brainflow/board_shim.py b/python_package/brainflow/board_shim.py index ced6ec9d5..34ca5c5e4 100644 --- a/python_package/brainflow/board_shim.py +++ b/python_package/brainflow/board_shim.py @@ -74,6 +74,8 @@ class BoardIds(enum.IntEnum): EXPLORE_PLUS_8_CHAN_BOARD = 54 #: EXPLORE_PLUS_32_CHAN_BOARD = 55 #: PIEEG_BOARD = 56 #: + SYNCHRONI_3_CHANNELS_BOARD = 57 #: + SYNCHRONI_8_CHANNELS_BOARD = 58 #: class IpProtocolTypes(enum.IntEnum): diff --git a/src/board_controller/board_controller.cpp b/src/board_controller/board_controller.cpp index 623499463..0b0c172c1 100644 --- a/src/board_controller/board_controller.cpp +++ b/src/board_controller/board_controller.cpp @@ -52,6 +52,7 @@ #include "pieeg_board.h" #include "playback_file_board.h" #include "streaming_board.h" +#include "synchroni_board.h" #include "synthetic_board.h" #include "unicorn_board.h" @@ -281,6 +282,12 @@ int prepare_session (int board_id, const char *json_brainflow_input_params) case BoardIds::PIEEG_BOARD: board = std::shared_ptr (new PIEEGBoard (board_id, params)); break; + case BoardIds::SYNCHRONI_3_CHANNELS_BOARD: + board = std::shared_ptr (new SynchroniBoard (board_id, params)); + break; + case BoardIds::SYNCHRONI_8_CHANNELS_BOARD: + board = std::shared_ptr (new SynchroniBoard (board_id, params)); + break; default: return (int)BrainFlowExitCodes::UNSUPPORTED_BOARD_ERROR; } diff --git a/src/board_controller/brainflow_boards.cpp b/src/board_controller/brainflow_boards.cpp index 22d5c91f4..543f4e074 100644 --- a/src/board_controller/brainflow_boards.cpp +++ b/src/board_controller/brainflow_boards.cpp @@ -74,7 +74,9 @@ BrainFlowBoards::BrainFlowBoards() {"53", json::object()}, {"54", json::object()}, {"55", json::object()}, - {"56", json::object()} + {"56", json::object()}, + {"57", json::object()}, + {"58", json::object()} } }}; @@ -1095,6 +1097,27 @@ BrainFlowBoards::BrainFlowBoards() {"eeg_channels", {1, 2, 3, 4, 5, 6, 7, 8}}, {"eeg_names", "Fp1,Fp2,C3,C4,P7,P8,O1,O2"} }; + // TODO update: + brainflow_boards_json["boards"]["57"]["default"] = { + {"name", "Synchroni3Channels"}, + {"sampling_rate", 250}, + {"package_num_channel", 0}, + {"timestamp_channel", 4}, + {"marker_channel", 5}, + {"num_rows", 6}, + {"eeg_channels", {1, 2}}, + {"ecg_channels", {3}} + }; + brainflow_boards_json["boards"]["58"]["default"] = { + {"name", "Synchroni8Channels"}, + {"sampling_rate", 250}, + {"package_num_channel", 0}, + {"timestamp_channel", 9}, + {"marker_channel", 10}, + {"num_rows", 11}, + {"eeg_channels", {1, 2, 3, 4, 5, 6, 7}}, + {"ecg_channels", {8}} + }; } BrainFlowBoards boards_struct; diff --git a/src/board_controller/build.cmake b/src/board_controller/build.cmake index aeeedbaa7..7baa52189 100644 --- a/src/board_controller/build.cmake +++ b/src/board_controller/build.cmake @@ -85,6 +85,7 @@ SET (BOARD_CONTROLLER_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/ntl/ntl_wifi.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/aavaa/aavaa_v3.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/pieeg/pieeg_board.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/synchroni/synchroni_board.cpp ) include (${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/ant_neuro/build.cmake) @@ -142,6 +143,7 @@ target_include_directories ( ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/ntl/inc ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/aavaa/inc ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/pieeg/inc + ${CMAKE_CURRENT_SOURCE_DIR}/src/board_controller/synchroni/inc ) target_compile_definitions(${BOARD_CONTROLLER_NAME} PRIVATE NOMINMAX BRAINFLOW_VERSION=${BRAINFLOW_VERSION}) diff --git a/src/board_controller/synchroni/inc/synchroni_board.h b/src/board_controller/synchroni/inc/synchroni_board.h new file mode 100644 index 000000000..bde041f5b --- /dev/null +++ b/src/board_controller/synchroni/inc/synchroni_board.h @@ -0,0 +1,36 @@ +#include +#include +#include + +#include "ble_lib_board.h" +#include "board.h" +#include "board_controller.h" + +class SynchroniBoard : public BLELibBoard +{ + +public: + SynchroniBoard (int board_id, struct BrainFlowInputParams params); + ~SynchroniBoard (); + + int prepare_session (); + int start_stream (int buffer_size, const char *streamer_params); + int stop_stream (); + int release_session (); + int config_board (std::string config, std::string &response); + int config_board (std::string config); + + void adapter_1_on_scan_found (simpleble_adapter_t adapter, simpleble_peripheral_t peripheral); + void read_data ( + simpleble_uuid_t service, simpleble_uuid_t characteristic, uint8_t *data, size_t size); + +protected: + volatile simpleble_adapter_t synchroni_board_adapter; + volatile simpleble_peripheral_t synchroni_board_peripheral; + bool initialized; + bool is_streaming; + std::mutex m; + std::condition_variable cv; + std::pair notified_characteristics; + std::pair write_characteristics; +}; diff --git a/src/board_controller/synchroni/synchroni_board.cpp b/src/board_controller/synchroni/synchroni_board.cpp new file mode 100644 index 000000000..fb5204ad3 --- /dev/null +++ b/src/board_controller/synchroni/synchroni_board.cpp @@ -0,0 +1,380 @@ +#include + +#include "synchroni_board.h" + +#include "custom_cast.h" +#include "get_dll_dir.h" +#include "timestamp.h" + +#define SYNCHRONI_WRITE_CHAR "0000fe41-8e22-4541-9d4c-21edae82ed19" +#define SYNCHRONI_NOTIFY_CHAR "0000fe42-8e22-4541-9d4c-21edae82ed19" + + +static void synchroni_board_adapter_1_on_scan_found ( + simpleble_adapter_t adapter, simpleble_peripheral_t peripheral, void *board) +{ + ((SynchroniBoard *)(board))->adapter_1_on_scan_found (adapter, peripheral); +} + +static void synchroni_board_read_notifications (simpleble_uuid_t service, + simpleble_uuid_t characteristic, uint8_t *data, size_t size, void *board) +{ + ((SynchroniBoard *)(board))->read_data (service, characteristic, data, size); +} + +SynchroniBoard::SynchroniBoard (int board_id, struct BrainFlowInputParams params) + : BLELibBoard (board_id, params) +{ + initialized = false; + synchroni_board_adapter = NULL; + synchroni_board_peripheral = NULL; + is_streaming = false; +} + +SynchroniBoard::~SynchroniBoard () +{ + skip_logs = true; + release_session (); +} + +int SynchroniBoard::prepare_session () +{ + if (initialized) + { + safe_logger (spdlog::level::info, "Session is already prepared"); + return (int)BrainFlowExitCodes::STATUS_OK; + } + if (params.timeout < 1) + { + params.timeout = 5; + } + safe_logger (spdlog::level::info, "Use timeout for discovery: {}", params.timeout); + if (!init_dll_loader ()) + { + safe_logger (spdlog::level::err, "Failed to init dll_loader"); + return (int)BrainFlowExitCodes::GENERAL_ERROR; + } + size_t adapter_count = simpleble_adapter_get_count (); + if (adapter_count == 0) + { + safe_logger (spdlog::level::err, "No BLE adapters found"); + return (int)BrainFlowExitCodes::UNABLE_TO_OPEN_PORT_ERROR; + } + + synchroni_board_adapter = simpleble_adapter_get_handle (0); + if (synchroni_board_adapter == NULL) + { + safe_logger (spdlog::level::err, "Adapter is NULL"); + return (int)BrainFlowExitCodes::UNABLE_TO_OPEN_PORT_ERROR; + } + + simpleble_adapter_set_callback_on_scan_found ( + synchroni_board_adapter, ::synchroni_board_adapter_1_on_scan_found, (void *)this); + + if (!simpleble_adapter_is_bluetooth_enabled ()) + { + safe_logger (spdlog::level::warn, "Probably bluetooth is disabled."); + // dont throw an exception because of this + // https://github.com/OpenBluetoothToolbox/SimpleBLE/issues/115 + } + + simpleble_adapter_scan_start (synchroni_board_adapter); + int res = (int)BrainFlowExitCodes::STATUS_OK; + std::unique_lock lk (m); + auto sec = std::chrono::seconds (1); + if (cv.wait_for ( + lk, params.timeout * sec, [this] { return this->synchroni_board_peripheral != NULL; })) + { + safe_logger (spdlog::level::info, "Found SynchroniBoard device"); + } + else + { + safe_logger (spdlog::level::err, "Failed to find SynchroniBoard Device"); + res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + } + simpleble_adapter_scan_stop (synchroni_board_adapter); + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + // for safety + for (int i = 0; i < 3; i++) + { + if (simpleble_peripheral_connect (synchroni_board_peripheral) == SIMPLEBLE_SUCCESS) + { + safe_logger (spdlog::level::info, "Connected to SynchroniBoard Device"); + res = (int)BrainFlowExitCodes::STATUS_OK; + + break; + } + else + { + safe_logger ( + spdlog::level::warn, "Failed to connect to SynchroniBoard Device: {}/3", i); + res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; +#ifdef _WIN32 + Sleep (1000); +#else + sleep (1); +#endif + } + } + } + else + { +// https://github.com/OpenBluetoothToolbox/SimpleBLE/issues/26#issuecomment-955606799 +#ifdef __linux__ + usleep (1000000); +#endif + } + + bool control_characteristics_found = false; + + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + size_t services_count = simpleble_peripheral_services_count (synchroni_board_peripheral); + for (size_t i = 0; i < services_count; i++) + { + simpleble_service_t service; + if (simpleble_peripheral_services_get (synchroni_board_peripheral, i, &service) != + SIMPLEBLE_SUCCESS) + { + safe_logger (spdlog::level::err, "failed to get service"); + res = (int)BrainFlowExitCodes::BOARD_NOT_READY_ERROR; + } + + safe_logger (spdlog::level::trace, "found servce {}", service.uuid.value); + for (size_t j = 0; j < service.characteristic_count; j++) + { + safe_logger (spdlog::level::trace, "found characteristic {}", + service.characteristics[j].uuid.value); + + if (strcmp (service.characteristics[j].uuid.value, + SYNCHRONI_WRITE_CHAR) == 0) // Write Characteristics + { + write_characteristics = std::pair ( + service.uuid, service.characteristics[j].uuid); + control_characteristics_found = true; + safe_logger (spdlog::level::info, "found control characteristic"); + } + if (strcmp (service.characteristics[j].uuid.value, + SYNCHRONI_NOTIFY_CHAR) == 0) // Notification Characteristics + { + if (simpleble_peripheral_notify (synchroni_board_peripheral, service.uuid, + service.characteristics[j].uuid, ::synchroni_board_read_notifications, + (void *)this) == SIMPLEBLE_SUCCESS) + { + + notified_characteristics = std::pair ( + service.uuid, service.characteristics[j].uuid); + } + else + { + safe_logger (spdlog::level::err, "Failed to notify for {} {}", + service.uuid.value, service.characteristics[j].uuid.value); + res = (int)BrainFlowExitCodes::GENERAL_ERROR; + } + } + } + } + } + + if ((res == (int)BrainFlowExitCodes::STATUS_OK) && (control_characteristics_found)) + { + initialized = true; + } + else + { + release_session (); + } + return res; +} + +int SynchroniBoard::start_stream (int buffer_size, const char *streamer_params) +{ + if (!initialized) + { + return (int)BrainFlowExitCodes::BOARD_NOT_CREATED_ERROR; + } + int res = prepare_for_acquisition (buffer_size, streamer_params); + + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + // todo send smth to enable streaming + res = config_board (""); + } + if (res == (int)BrainFlowExitCodes::STATUS_OK) + { + is_streaming = true; + } + + return res; +} + +int SynchroniBoard::stop_stream () +{ + if (synchroni_board_peripheral == NULL) + { + return (int)BrainFlowExitCodes::BOARD_NOT_CREATED_ERROR; + } + int res = (int)BrainFlowExitCodes::STATUS_OK; + if (is_streaming) + { + // todo send smth to stop streaming + res = config_board (""); + } + else + { + res = (int)BrainFlowExitCodes::STREAM_ALREADY_RUN_ERROR; + } + is_streaming = false; + return res; +} + +int SynchroniBoard::release_session () +{ + if (initialized) + { + // repeat it multiple times, failure here may lead to a crash + for (int i = 0; i < 2; i++) + { + stop_stream (); + // need to wait for notifications to stop triggered before unsubscribing, otherwise + // macos fails inside simpleble with timeout +#ifdef _WIN32 + Sleep (2000); +#else + sleep (2); +#endif + if (simpleble_peripheral_unsubscribe (synchroni_board_peripheral, + notified_characteristics.first, + notified_characteristics.second) != SIMPLEBLE_SUCCESS) + { + safe_logger (spdlog::level::err, "failed to unsubscribe for {} {}", + notified_characteristics.first.value, notified_characteristics.second.value); + } + else + { + break; + } + } + free_packages (); + initialized = false; + } + if (synchroni_board_peripheral != NULL) + { + bool is_connected = false; + if (simpleble_peripheral_is_connected (synchroni_board_peripheral, &is_connected) == + SIMPLEBLE_SUCCESS) + { + if (is_connected) + { + simpleble_peripheral_disconnect (synchroni_board_peripheral); + } + } + simpleble_peripheral_release_handle (synchroni_board_peripheral); + synchroni_board_peripheral = NULL; + } + if (synchroni_board_adapter != NULL) + { + simpleble_adapter_release_handle (synchroni_board_adapter); + synchroni_board_adapter = NULL; + } + + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int SynchroniBoard::config_board (std::string config, std::string &response) +{ + return config_board (config); +} + +int SynchroniBoard::config_board (std::string config) +{ + if (!initialized) + { + return (int)BrainFlowExitCodes::BOARD_NOT_CREATED_ERROR; + } + + uint8_t *command = new uint8_t[config.size ()]; + memcpy (command, config.c_str (), config.size ()); + if (simpleble_peripheral_write_command (synchroni_board_peripheral, write_characteristics.first, + write_characteristics.second, command, config.size ()) != SIMPLEBLE_SUCCESS) + { + safe_logger (spdlog::level::err, "failed to send command {} to device", config.c_str ()); + delete[] command; + return (int)BrainFlowExitCodes::BOARD_WRITE_ERROR; + } + delete[] command; + return (int)BrainFlowExitCodes::STATUS_OK; +} + +void SynchroniBoard::adapter_1_on_scan_found ( + simpleble_adapter_t adapter, simpleble_peripheral_t peripheral) +{ + char *peripheral_identified = simpleble_peripheral_identifier (peripheral); + char *peripheral_address = simpleble_peripheral_address (peripheral); + simpleble_manufacturer_data_t manufacturer_data; + bool found = false; + + + size_t data_count = simpleble_peripheral_manufacturer_data_count (peripheral); + for (size_t i = 0; i < data_count; i++) + { + if (simpleble_peripheral_manufacturer_data_get (peripheral, i, &manufacturer_data) == + SIMPLEBLE_SUCCESS) + { + safe_logger (spdlog::level::info, "id: {}", manufacturer_data.manufacturer_id); + for (size_t j = 0; j < manufacturer_data.data_length; j++) + { + safe_logger (spdlog::level::info, "data: {} {}", j, manufacturer_data.data[j]); + } + } + } + if (!params.mac_address.empty ()) + { + if (strcmp (peripheral_address, params.mac_address.c_str ()) == 0) + { + found = true; + } + } + else + { + if (!params.serial_number.empty ()) + { + if (strcmp (peripheral_identified, params.serial_number.c_str ()) == 0) + { + found = true; + } + } + else + { + if (strncmp (peripheral_identified, "BA_FLEX", 7) == 0) + { + found = true; + } + } + } + + safe_logger (spdlog::level::trace, "address {}", peripheral_address); + simpleble_free (peripheral_address); + safe_logger (spdlog::level::trace, "identifier {}", peripheral_identified); + simpleble_free (peripheral_identified); + + if (found) + { + { + std::lock_guard lk (m); + synchroni_board_peripheral = peripheral; + } + cv.notify_one (); + } + else + { + simpleble_peripheral_release_handle (peripheral); + } +} + +void SynchroniBoard::read_data ( + simpleble_uuid_t service, simpleble_uuid_t characteristic, uint8_t *data, size_t size) +{ + // todo implement +} diff --git a/src/utils/inc/brainflow_constants.h b/src/utils/inc/brainflow_constants.h index 26034af5b..85b764bb6 100644 --- a/src/utils/inc/brainflow_constants.h +++ b/src/utils/inc/brainflow_constants.h @@ -88,9 +88,11 @@ enum class BoardIds : int EXPLORE_PLUS_8_CHAN_BOARD = 54, EXPLORE_PLUS_32_CHAN_BOARD = 55, PIEEG_BOARD = 56, + SYNCHRONI_3_CHANNELS_BOARD = 57, + SYNCHRONI_8_CHANNELS_BOARD = 58, // use it to iterate FIRST = PLAYBACK_FILE_BOARD, - LAST = PIEEG_BOARD + LAST = SYNCHRONI_8_CHANNELS_BOARD }; enum class IpProtocolTypes : int diff --git a/third_party/SimpleBLE/.github/FUNDING.yml b/third_party/SimpleBLE/.github/FUNDING.yml new file mode 100644 index 000000000..f6ab0f4d8 --- /dev/null +++ b/third_party/SimpleBLE/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: [kdewald] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/third_party/SimpleBLE/.github/workflows/ci_build_examples.yml b/third_party/SimpleBLE/.github/workflows/ci_build_examples.yml index 8794effc7..9dc285e75 100644 --- a/third_party/SimpleBLE/.github/workflows/ci_build_examples.yml +++ b/third_party/SimpleBLE/.github/workflows/ci_build_examples.yml @@ -6,59 +6,40 @@ jobs: # ------------------------------------------------------------ build-windows: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - max-parallel: 4 - matrix: - os: [windows-2019] - # For windows-2022, the generator is for "Visual Studio 17 2022" + runs-on: windows-2022 steps: - name: Clone Repository uses: actions/checkout@v3 - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.0.2 - - name: Compile Examples for Windows + uses: microsoft/setup-msbuild@v1.1 + - name: Compile Examples run: | - cmake -B %GITHUB_WORKSPACE%\build_simpleble_examples -G "Visual Studio 16 2019" -DCMAKE_SYSTEM_VERSION=10.0.22000.0 -S %GITHUB_WORKSPACE%/examples/simpleble - cmake --build %GITHUB_WORKSPACE%\build_simpleble_examples --config Release --parallel 4 + cmake -B %GITHUB_WORKSPACE%\build -G "Visual Studio 17 2022" -DCMAKE_SYSTEM_VERSION=10.0.22000.0 -S %GITHUB_WORKSPACE%/examples/simpleble + cmake --build %GITHUB_WORKSPACE%\build --config Release --parallel 4 shell: cmd # ------------------------------------------------------------ build-macos: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - max-parallel: 4 - matrix: - os: [macos-11.0] + runs-on: macos-12 steps: - name: Clone Repository uses: actions/checkout@v3 - name: Setup Cmake - uses: jwlawson/actions-setup-cmake@v1.12 + uses: jwlawson/actions-setup-cmake@v1.13 with: - cmake-version: '3.21.x' - - name: Compile Examples for MacOS + cmake-version: '3.21' + - name: Compile Examples run: | - cmake -B $GITHUB_WORKSPACE/build_simpleble_examples -DCMAKE_BUILD_TYPE=Release -S $GITHUB_WORKSPACE/examples/simpleble - cmake --build $GITHUB_WORKSPACE/build_simpleble_examples --config Release --parallel 4 + cmake -B $GITHUB_WORKSPACE/build -DCMAKE_BUILD_TYPE=Release -S $GITHUB_WORKSPACE/examples/simpleble + cmake --build $GITHUB_WORKSPACE/build --config Release --parallel 4 # ------------------------------------------------------------ build-linux: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - max-parallel: 4 - matrix: - os: [ubuntu-20.04] + runs-on: ubuntu-22.04 steps: - name: Clone Repository @@ -70,9 +51,9 @@ jobs: env: DEBIAN_FRONTEND: noninteractive - name: Setup Cmake - uses: jwlawson/actions-setup-cmake@v1.12 + uses: jwlawson/actions-setup-cmake@v1.13 with: - cmake-version: '3.21.x' + cmake-version: '3.21' - name: Compile SimpleBLE Examples for Ubuntu run: | cmake -B $GITHUB_WORKSPACE/build_simpleble_examples -DCMAKE_BUILD_TYPE=Release -S $GITHUB_WORKSPACE/examples/simpleble @@ -84,4 +65,4 @@ jobs: - name: Compile SimpleDBus Examples for Ubuntu run: | cmake -B $GITHUB_WORKSPACE/build_simpledbus_examples -DCMAKE_BUILD_TYPE=Release -S $GITHUB_WORKSPACE/examples/simpledbus - cmake --build $GITHUB_WORKSPACE/build_simpledbus_examples --config Release --parallel 4 \ No newline at end of file + cmake --build $GITHUB_WORKSPACE/build_simpledbus_examples --config Release --parallel 4 diff --git a/third_party/SimpleBLE/.github/workflows/ci_build_lib.yml b/third_party/SimpleBLE/.github/workflows/ci_build_lib.yml deleted file mode 100644 index bcfc26fea..000000000 --- a/third_party/SimpleBLE/.github/workflows/ci_build_lib.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: CI Build Libs - -on: [push, pull_request] - -jobs: - - # ------------------------------------------------------------ - build-windows: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - max-parallel: 4 - matrix: - os: [windows-2019] - # For windows-2022, the generator is for "Visual Studio 17 2022" - - steps: - - name: Clone Repository - uses: actions/checkout@v3 - - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.0.2 - - name: Compile SimpleBLE for Windows x86 - Windows SDK 10.0.19041.0 - run: | - cmake -B %GITHUB_WORKSPACE%\build32_19041 -G "Visual Studio 16 2019" -A Win32 -DCMAKE_SYSTEM_VERSION=10.0.19041.0 -S %GITHUB_WORKSPACE%/simpleble - cmake --build %GITHUB_WORKSPACE%\build32_19041 --config Release --parallel 4 - cmake --install %GITHUB_WORKSPACE%\build32_19041 --prefix %GITHUB_WORKSPACE%\build32_19041\install - shell: cmd - - name: Compile SimpleBLE for Windows x64 - Windows SDK 10.0.19041.0 - run: | - cmake -B %GITHUB_WORKSPACE%\build64_19041 -G "Visual Studio 16 2019" -A x64 -DCMAKE_SYSTEM_VERSION=10.0.19401.0 -S %GITHUB_WORKSPACE%/simpleble - cmake --build %GITHUB_WORKSPACE%\build64_19041 --config Release --parallel 4 - cmake --install %GITHUB_WORKSPACE%\build64_19041 --prefix %GITHUB_WORKSPACE%\build64_19041\install - shell: cmd - - name: Compile SimpleBLE for Windows x86 - Windows SDK 10.0.22000.0 - run: | - cmake -B %GITHUB_WORKSPACE%\build32 -G "Visual Studio 16 2019" -A Win32 -DCMAKE_SYSTEM_VERSION=10.0.22000.0 -S %GITHUB_WORKSPACE%/simpleble - cmake --build %GITHUB_WORKSPACE%\build32 --config Release --parallel 4 - cmake --install %GITHUB_WORKSPACE%\build32 --prefix %GITHUB_WORKSPACE%\build32\install - shell: cmd - - name: Compile SimpleBLE for Windows x64 - Windows SDK 10.0.22000.0 - run: | - cmake -B %GITHUB_WORKSPACE%\build64 -G "Visual Studio 16 2019" -A x64 -DCMAKE_SYSTEM_VERSION=10.0.22000.0 -S %GITHUB_WORKSPACE%/simpleble - cmake --build %GITHUB_WORKSPACE%\build64 --config Release --parallel 4 - cmake --install %GITHUB_WORKSPACE%\build64 --prefix %GITHUB_WORKSPACE%\build64\install - shell: cmd - - # ------------------------------------------------------------ - - build-macos: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - max-parallel: 4 - matrix: - os: [macos-11.0] - - steps: - - name: Clone Repository - uses: actions/checkout@v3 - - name: Setup Cmake - uses: jwlawson/actions-setup-cmake@v1.12 - with: - cmake-version: '3.21.x' - - name: Compile SimpleBLE for MacOS arm64 - run: | - cmake -B $GITHUB_WORKSPACE/build_arm64 -DCMAKE_OSX_ARCHITECTURES="arm64" -DCMAKE_BUILD_TYPE=Release -S $GITHUB_WORKSPACE/simpleble - cmake --build $GITHUB_WORKSPACE/build_arm64 --config Release --parallel 4 - cmake --install $GITHUB_WORKSPACE/build_arm64 --prefix $GITHUB_WORKSPACE/build_arm64/install - - name: Compile SimpleBLE for MacOS x86 - run: | - cmake -B $GITHUB_WORKSPACE/build_x86 -DCMAKE_OSX_ARCHITECTURES="x86_64" -DCMAKE_BUILD_TYPE=Release -S $GITHUB_WORKSPACE/simpleble - cmake --build $GITHUB_WORKSPACE/build_x86 --config Release --parallel 4 - cmake --install $GITHUB_WORKSPACE/build_x86 --prefix $GITHUB_WORKSPACE/build_x86/install - - # ------------------------------------------------------------ - - build-linux: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - max-parallel: 4 - matrix: - os: [ubuntu-20.04] - - steps: - - name: Clone Repository - uses: actions/checkout@v3 - - name: Install Dependencies - run: | - sudo -H apt-get update -y - sudo -H apt-get install -y libdbus-1-dev - env: - DEBIAN_FRONTEND: noninteractive - - name: Setup Cmake - uses: jwlawson/actions-setup-cmake@v1.12 - with: - cmake-version: '3.21.x' - - name: Compile SimpleBLE for Ubuntu - run: | - cmake -B $GITHUB_WORKSPACE/build_simpleble -DCMAKE_BUILD_TYPE=Release -S $GITHUB_WORKSPACE/simpleble - cmake --build $GITHUB_WORKSPACE/build_simpleble --config Release --parallel 4 - cmake --install $GITHUB_WORKSPACE/build_simpleble --prefix $GITHUB_WORKSPACE/build_simpleble/install - - name: Compile SimpleBluez for Ubuntu - run: | - cmake -B $GITHUB_WORKSPACE/build_simplebluez -DCMAKE_BUILD_TYPE=Release -S $GITHUB_WORKSPACE/simplebluez - cmake --build $GITHUB_WORKSPACE/build_simplebluez --config Release --parallel 4 - cmake --install $GITHUB_WORKSPACE/build_simplebluez --prefix $GITHUB_WORKSPACE/build_simplebluez/install - - name: Compile SimpleDBus for Ubuntu - run: | - cmake -B $GITHUB_WORKSPACE/build_simpledbus -DCMAKE_BUILD_TYPE=Release -S $GITHUB_WORKSPACE/simpledbus - cmake --build $GITHUB_WORKSPACE/build_simpledbus --config Release --parallel 4 - cmake --install $GITHUB_WORKSPACE/build_simpledbus --prefix $GITHUB_WORKSPACE/build_simpledbus/install diff --git a/third_party/SimpleBLE/.github/workflows/ci_cpp_release.yml b/third_party/SimpleBLE/.github/workflows/ci_cpp_release.yml new file mode 100644 index 000000000..b47c441b5 --- /dev/null +++ b/third_party/SimpleBLE/.github/workflows/ci_cpp_release.yml @@ -0,0 +1,205 @@ +name: Cpp Release + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "The tag to upload artifacts" + required: true + +jobs: + + # ------------------------------------------------------------ + + cpp-release-linux: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + max-parallel: 4 + matrix: + options: [ + {container: dockcross/linux-x64, target: linux-x64}, + {container: dockcross/linux-x86, target: linux-x86}, + {container: dockcross/linux-armv6-lts, target: linux-armv6}, + ] + type: [shared, static] + + container: + image: ${{ matrix.options.container }} + steps: + - name: Clone repository + uses: actions/checkout@v3 + env: + TAG: ${{ inputs.tag || github.ref }} + with: + ref: ${{ env.TAG }} + + - name: Build Expat + run: | + git clone https://github.com/libexpat/libexpat.git /tmp/libexpat + cd /tmp/libexpat/expat + + cmake -B /tmb/expat/build -S /tmp/libexpat/expat -DEXPAT_BUILD_DOCS=OFF -DEXPAT_BUILD_EXAMPLES=OFF -DEXPAT_BUILD_TESTS=OFF + cmake --build /tmb/expat/build --config Release --parallel 4 + cmake --install /tmb/expat/build --prefix /tmp/expat/install + + - name: Build DBus + run: | + git clone https://gitlab.freedesktop.org/dbus/dbus.git /tmp/dbus + + export CMAKE_PREFIX_PATH=/tmp/expat/install:$CMAKE_PREFIX_PATH + + cmake -B /tmb/dbus/build -S /tmp/dbus -DDBUS_SESSION_SOCKET_DIR=/usr/local/var/run/dbus/system_bus_socket -DDBUS_BUILD_TESTS=OFF + cmake --build /tmb/dbus/build --config Release --parallel 4 + cmake --install /tmb/dbus/build --prefix /tmp/dbus/install + + - name: Build SimpleBLE + run: | + GITHUB_WORKSPACE=$(pwd) + + if [ "${{ matrix.type }}" = "shared" ]; then + BUILD_SHARED_LIBS=ON + else + BUILD_SHARED_LIBS=OFF + fi + + export CMAKE_PREFIX_PATH=/tmp/dbus/install:$CMAKE_PREFIX_PATH + + cmake -B $GITHUB_WORKSPACE/build_simpleble -DCMAKE_BUILD_TYPE=Release -S $GITHUB_WORKSPACE/simpleble -DBUILD_SHARED_LIBS=$BUILD_SHARED_LIBS + cmake --build $GITHUB_WORKSPACE/build_simpleble --config Release --parallel 4 + cmake --install $GITHUB_WORKSPACE/build_simpleble --prefix $GITHUB_WORKSPACE/build_simpleble/install + + mkdir -p $GITHUB_WORKSPACE/artifacts + zip -r $GITHUB_WORKSPACE/artifacts/simpleble_${{ matrix.type }}_${{ matrix.options.target }}.zip $GITHUB_WORKSPACE/build_simpleble/install + + - name: Upload binaries to job + uses: actions/upload-artifact@v3 + with: + name: simpleble_${{ matrix.type }}_${{ matrix.options.target }} + path: artifacts/simpleble_${{ matrix.type }}_${{ matrix.options.target }}.zip + + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v2 + env: + TAG: ${{ inputs.tag || github.ref }} + with: + file: ${{ github.workspace }}/artifacts/**/* + tag: ${{ env.TAG }} + overwrite: true + file_glob: true + + # ------------------------------------------------------------ + + cpp-release-windows: + runs-on: windows-2022 + + strategy: + fail-fast: false + max-parallel: 4 + matrix: + arch: [Win32, x64] + type: [shared, static] + + steps: + - name: Clone repository + uses: actions/checkout@v3 + env: + TAG: ${{ inputs.tag || github.ref }} + with: + ref: ${{ env.TAG }} + + - name: Add msbuild to PATH + uses: microsoft/setup-msbuild@v1.1 + + - name: Compile SimpleBLE + shell: cmd + run: | + + if "${{ matrix.type }}" == "shared" ( + set BUILD_SHARED_LIBS=ON + ) else ( + set BUILD_SHARED_LIBS=OFF + ) + + cmake -B %GITHUB_WORKSPACE%\build -G "Visual Studio 17 2022" -A ${{ matrix.arch }} -DCMAKE_SYSTEM_VERSION="10.0.19041.0" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=%BUILD_SHARED_LIBS% -S %GITHUB_WORKSPACE%/simpleble + cmake --build %GITHUB_WORKSPACE%\build --config Release --parallel 4 + cmake --install %GITHUB_WORKSPACE%\build --prefix %GITHUB_WORKSPACE%\build\install + + mkdir -p $GITHUB_WORKSPACE\artifacts + 7z a -tzip %GITHUB_WORKSPACE%\artifacts\simpleble_${{ matrix.type }}_windows-${{ matrix.arch }}.zip %GITHUB_WORKSPACE%\build\install + + - name: Upload binaries to job + uses: actions/upload-artifact@v3 + with: + name: simpleble_${{ matrix.type }}_windows-${{ matrix.arch }} + path: artifacts/simpleble_${{ matrix.type }}_windows-${{ matrix.arch }}.zip + + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v2 + env: + TAG: ${{ inputs.tag || github.ref }} + with: + file: ${{ github.workspace }}/artifacts/**/* + tag: ${{ env.TAG }} + overwrite: true + file_glob: true + + # ------------------------------------------------------------ + + cpp-release-macos: + runs-on: macos-12 + + strategy: + fail-fast: false + max-parallel: 4 + matrix: + arch: [arm64, x86_64] + type: [shared, static] + + steps: + - name: Clone repository + uses: actions/checkout@v3 + env: + TAG: ${{ inputs.tag || github.ref }} + with: + ref: ${{ env.TAG }} + + - name: Setup CMake + uses: jwlawson/actions-setup-cmake@v1.13 + with: + cmake-version: '3.21' + + - name: Compile SimpleBLE + run: | + + if [ "${{ matrix.type }}" = "shared" ]; then + BUILD_SHARED_LIBS=ON + else + BUILD_SHARED_LIBS=OFF + fi + + cmake -B $GITHUB_WORKSPACE/build -DCMAKE_OSX_ARCHITECTURES="${{ matrix.arch }}" -DCMAKE_BUILD_TYPE=Release -S $GITHUB_WORKSPACE/simpleble -DBUILD_SHARED_LIBS=$BUILD_SHARED_LIBS + cmake --build $GITHUB_WORKSPACE/build --config Release --parallel 4 + cmake --install $GITHUB_WORKSPACE/build --prefix $GITHUB_WORKSPACE/build/install + + mkdir -p $GITHUB_WORKSPACE/artifacts + zip -r $GITHUB_WORKSPACE/artifacts/simpleble_${{ matrix.type }}_macos-${{ matrix.arch }}.zip $GITHUB_WORKSPACE/build/install + + - name: Upload binaries to job + uses: actions/upload-artifact@v3 + with: + name: simpleble_${{ matrix.type }}_macos-${{ matrix.arch }} + path: artifacts/simpleble_${{ matrix.type }}_macos-${{ matrix.arch }}.zip + + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v2 + env: + TAG: ${{ inputs.tag || github.ref }} + with: + file: ${{ github.workspace }}/artifacts/**/* + tag: ${{ env.TAG }} + overwrite: true + file_glob: true diff --git a/third_party/SimpleBLE/.github/workflows/ci_cpp_release_test.yml b/third_party/SimpleBLE/.github/workflows/ci_cpp_release_test.yml new file mode 100644 index 000000000..2d4edbb2a --- /dev/null +++ b/third_party/SimpleBLE/.github/workflows/ci_cpp_release_test.yml @@ -0,0 +1,163 @@ +name: Cpp Release Test + +on: + # push: + # pull_request: + workflow_dispatch: + +jobs: + # ------------------------------------------------------------ + + cpp-release-linux: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + max-parallel: 4 + matrix: + library: [ + {lib: simpledbus, name: SimpleDBus}, + {lib: simplebluez, name: SimpleBluez}, + {lib: simpleble, name: SimpleBLE}, + ] + options: [ + {container: dockcross/linux-x64, target: linux-x64}, + {container: dockcross/linux-x86, target: linux-x86}, + {container: dockcross/linux-armv6-lts, target: linux-armv6}, + ] + type: [shared, static] + + container: + image: ${{ matrix.options.container }} + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Build Expat + run: | + git clone https://github.com/libexpat/libexpat.git /tmp/libexpat + cd /tmp/libexpat/expat + + cmake -B /tmb/expat/build -S /tmp/libexpat/expat -DEXPAT_BUILD_DOCS=OFF -DEXPAT_BUILD_EXAMPLES=OFF -DEXPAT_BUILD_TESTS=OFF + cmake --build /tmb/expat/build --config Release --parallel 4 + cmake --install /tmb/expat/build --prefix /tmp/expat/install + + - name: Build DBus + run: | + git clone https://gitlab.freedesktop.org/dbus/dbus.git /tmp/dbus + + export CMAKE_PREFIX_PATH=/tmp/expat/install:$CMAKE_PREFIX_PATH + + cmake -B /tmb/dbus/build -S /tmp/dbus -DDBUS_SESSION_SOCKET_DIR=/usr/local/var/run/dbus/system_bus_socket -DDBUS_BUILD_TESTS=OFF + cmake --build /tmb/dbus/build --config Release --parallel 4 + cmake --install /tmb/dbus/build --prefix /tmp/dbus/install + + - name: Build ${{ matrix.library.name }} + run: | + GITHUB_WORKSPACE=$(pwd) + + if [ "${{ matrix.type }}" = "shared" ]; then + BUILD_SHARED_LIBS=ON + else + BUILD_SHARED_LIBS=OFF + fi + + export CMAKE_PREFIX_PATH=/tmp/dbus/install:$CMAKE_PREFIX_PATH + + cmake -B $GITHUB_WORKSPACE/build_${{ matrix.library.lib }} -DCMAKE_BUILD_TYPE=Release -S $GITHUB_WORKSPACE/${{ matrix.library.lib }} -DBUILD_SHARED_LIBS=$BUILD_SHARED_LIBS + cmake --build $GITHUB_WORKSPACE/build_${{ matrix.library.lib }} --config Release --parallel 4 + cmake --install $GITHUB_WORKSPACE/build_${{ matrix.library.lib }} --prefix $GITHUB_WORKSPACE/build_${{ matrix.library.lib }}/install + + mkdir -p $GITHUB_WORKSPACE/artifacts + zip -r $GITHUB_WORKSPACE/artifacts/${{ matrix.library.lib }}_${{ matrix.type }}_${{ matrix.options.target }}.zip $GITHUB_WORKSPACE/build_${{ matrix.library.lib }}/install + + - name: Upload binaries to job + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.library.lib }}_${{ matrix.type }}_${{ matrix.options.target }} + path: artifacts/${{ matrix.library.lib }}_${{ matrix.type }}_${{ matrix.options.target }}.zip + + # ------------------------------------------------------------ + + cpp-release-windows: + runs-on: windows-2022 + + strategy: + fail-fast: false + max-parallel: 4 + matrix: + arch: [Win32, x64] + type: [shared, static] + + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Add msbuild to PATH + uses: microsoft/setup-msbuild@v1.1 + + - name: Compile SimpleBLE + shell: cmd + run: | + + if "${{ matrix.type }}" == "shared" ( + set BUILD_SHARED_LIBS=ON + ) else ( + set BUILD_SHARED_LIBS=OFF + ) + + cmake -B %GITHUB_WORKSPACE%\build -G "Visual Studio 17 2022" -A ${{ matrix.arch }} -DCMAKE_SYSTEM_VERSION="10.0.19041.0" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=%BUILD_SHARED_LIBS% -S %GITHUB_WORKSPACE%/simpleble + cmake --build %GITHUB_WORKSPACE%\build --config Release --parallel 4 + cmake --install %GITHUB_WORKSPACE%\build --prefix %GITHUB_WORKSPACE%\build\install + + mkdir -p $GITHUB_WORKSPACE\artifacts + 7z a -tzip %GITHUB_WORKSPACE%\artifacts\simpleble_${{ matrix.type }}_windows-${{ matrix.arch }}.zip %GITHUB_WORKSPACE%\build\install + + - name: Upload binaries to job + uses: actions/upload-artifact@v3 + with: + name: simpleble_${{ matrix.type }}_windows-${{ matrix.arch }} + path: artifacts/simpleble_${{ matrix.type }}_windows-${{ matrix.arch }}.zip + + # ------------------------------------------------------------ + + cpp-release-macos: + runs-on: macos-12 + + strategy: + fail-fast: false + max-parallel: 4 + matrix: + arch: [arm64, x86_64] + type: [shared, static] + + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Setup CMake + uses: jwlawson/actions-setup-cmake@v1.13 + with: + cmake-version: '3.21' + + - name: Compile SimpleBLE + run: | + + if [ "${{ matrix.type }}" = "shared" ]; then + BUILD_SHARED_LIBS=ON + else + BUILD_SHARED_LIBS=OFF + fi + + cmake -B $GITHUB_WORKSPACE/build -DCMAKE_OSX_ARCHITECTURES="${{ matrix.arch }}" -DCMAKE_BUILD_TYPE=Release -S $GITHUB_WORKSPACE/simpleble -DBUILD_SHARED_LIBS=$BUILD_SHARED_LIBS + cmake --build $GITHUB_WORKSPACE/build --config Release --parallel 4 + cmake --install $GITHUB_WORKSPACE/build --prefix $GITHUB_WORKSPACE/build/install + + mkdir -p $GITHUB_WORKSPACE/artifacts + zip -r $GITHUB_WORKSPACE/artifacts/simpleble_${{ matrix.type }}_macos-${{ matrix.arch }}.zip $GITHUB_WORKSPACE/build/install + + - name: Upload binaries to job + uses: actions/upload-artifact@v3 + with: + name: simpleble_${{ matrix.type }}_macos-${{ matrix.arch }} + path: artifacts/simpleble_${{ matrix.type }}_macos-${{ matrix.arch }}.zip diff --git a/third_party/SimpleBLE/.github/workflows/ci_docs.yml b/third_party/SimpleBLE/.github/workflows/ci_docs.yml index e541a2332..fd14509c4 100644 --- a/third_party/SimpleBLE/.github/workflows/ci_docs.yml +++ b/third_party/SimpleBLE/.github/workflows/ci_docs.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: build-docs: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout @@ -15,10 +15,10 @@ jobs: sudo -H apt-get update -y sudo -H apt-get install -y doxygen libdbus-1-dev - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: 3.11 cache: "pip" - name: Install dependencies @@ -29,7 +29,7 @@ jobs: run: make html - name: Upload artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: path: docs/_build/* name: docs diff --git a/third_party/SimpleBLE/.github/workflows/ci_lint.yml b/third_party/SimpleBLE/.github/workflows/ci_lint.yml index de89e5af9..fd0be0cf6 100644 --- a/third_party/SimpleBLE/.github/workflows/ci_lint.yml +++ b/third_party/SimpleBLE/.github/workflows/ci_lint.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: clang-format: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout @@ -19,7 +19,7 @@ jobs: inplace: False cppcheck: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout @@ -39,7 +39,7 @@ jobs: run: cppcheck-htmlreport --title=SimpleBLE --file=cppcheck_res.xml --report-dir=report - name: Upload Report if: ${{ failure() }} - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v3 with: name: report path: report diff --git a/third_party/SimpleBLE/.github/workflows/ci_py_release.yml b/third_party/SimpleBLE/.github/workflows/ci_py_release.yml new file mode 100644 index 000000000..2adf92412 --- /dev/null +++ b/third_party/SimpleBLE/.github/workflows/ci_py_release.yml @@ -0,0 +1,133 @@ +name: PyPI Release + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "The tag to upload artifacts" + required: false + +jobs: + build_sdist: + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@v3 + env: + TAG: ${{ inputs.tag || github.ref }} + with: + ref: ${{ env.TAG }} + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + cache: "pip" + + - name: Build source distribution + run: | + pip3 install build twine + python3 -m build --sdist + + - name: Upload files + uses: actions/upload-artifact@v3 + with: + name: simplepyble + path: dist/*.tar.gz + + - name: Check packages + run: twine check dist/*.tar.gz + + - name: Publish packages + run: twine upload --skip-existing dist/*.tar.gz --verbose + env: + TWINE_USERNAME: ${{ secrets.PYPI_USER }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + + - name: Upload source to release + uses: svenstaro/upload-release-action@v2 + if: inputs.tag != '' && inputs.tag != null + env: + TAG: ${{ inputs.tag }} + with: + file: dist/*.tar.gz + tag: ${{ env.TAG }} + overwrite: true + file_glob: true + + build_wheels: + runs-on: ${{ matrix.options.os }} + + strategy: + matrix: + options: [ + {os: ubuntu-22.04, arch: x86_64}, + {os: ubuntu-22.04, arch: i686}, + {os: ubuntu-22.04, arch: aarch64}, + {os: windows-2022, arch: AMD64}, + {os: windows-2022, arch: x86}, + {os: macos-12 , arch: x86_64}, + {os: macos-12 , arch: universal2}, + {os: macos-12 , arch: arm64}, + ] + + steps: + - name: Clone repository + uses: actions/checkout@v3 + env: + TAG: ${{ inputs.tag || github.ref }} + with: + ref: ${{ env.TAG }} + + - name: Set up QEMU + if: runner.os == 'Linux' + uses: docker/setup-qemu-action@v2 + with: + platforms: arm64 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + cache: "pip" + + - name: Install dependencies + run: pip install -r simplepyble/requirements.txt + + - name: Build wheel + uses: pypa/cibuildwheel@v2.16.5 + env: + CIBW_BEFORE_ALL_LINUX: "yum update -y && yum group install -y \"Development Tools\" && yum install -y dbus-devel" + CIBW_ARCHS_LINUX: ${{ matrix.options.arch }} + CIBW_ARCHS_MACOS: ${{ matrix.options.arch }} + CIBW_ARCHS_WINDOWS: ${{ matrix.options.arch }} + CIBW_SKIP: "*musllinux_* pp*" + + - name: Upload wheels to job + uses: actions/upload-artifact@v3 + with: + name: simpleble-wheels + path: wheelhouse/*.whl + + - name: Check Packages + run: twine check wheelhouse/*.whl + + - name: Upload wheels to release + uses: svenstaro/upload-release-action@v2 + if: inputs.tag != '' && inputs.tag != null + env: + TAG: ${{ inputs.tag }} + with: + file: wheelhouse/*.whl + tag: ${{ env.TAG }} + overwrite: true + file_glob: true + + - name: Publish packages + run: twine upload --skip-existing wheelhouse/*.whl --verbose + env: + TWINE_USERNAME: ${{ secrets.PYPI_USER }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} diff --git a/third_party/SimpleBLE/.github/workflows/ci_py_release_test.yml b/third_party/SimpleBLE/.github/workflows/ci_py_release_test.yml new file mode 100644 index 000000000..e3c6d45b5 --- /dev/null +++ b/third_party/SimpleBLE/.github/workflows/ci_py_release_test.yml @@ -0,0 +1,98 @@ +name: PyPI Test Release + +on: + # push: + # pull_request: + workflow_dispatch: + +jobs: + build_sdist: + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + cache: "pip" + + - name: Build source distribution + run: | + pip3 install build twine + python3 -m build --sdist + + - name: Upload files + uses: actions/upload-artifact@v3 + with: + name: simplepyble + path: dist/*.tar.gz + + - name: Check packages + run: twine check dist/*.tar.gz + + - name: Publish packages + if: ${{ env.HAS_TWINE_USERNAME == 'true' }} + run: | + twine upload --repository testpypi --skip-existing dist/*.tar.gz --verbose + env: + HAS_TWINE_USERNAME: ${{ secrets.TEST_PYPI_USER != '' }} + TWINE_USERNAME: ${{ secrets.TEST_PYPI_USER }} + TWINE_PASSWORD: ${{ secrets.TEST_PYPI_PASSWORD }} + + build_wheels: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-22.04 , windows-2022, macos-12] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up QEMU + if: runner.os == 'Linux' + uses: docker/setup-qemu-action@v2 + with: + platforms: arm64 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + cache: "pip" + + - name: Install dependencies + run: pip install -r simplepyble/requirements.txt + + - name: Build wheel + uses: pypa/cibuildwheel@v2.16.5 + env: + CIBW_BUILD: cp39-* # Only build for Python 3.9 + CIBW_BUILD_VERBOSITY: 3 + CIBW_BEFORE_ALL_LINUX: "yum update -y && yum group install -y \"Development Tools\" && yum install -y dbus-devel" + CIBW_ARCHS_LINUX: x86_64 i686 aarch64 + CIBW_ARCHS_MACOS: x86_64 universal2 arm64 + CIBW_ARCHS_WINDOWS: AMD64 x86 + CIBW_SKIP: "*musllinux_* pp*" + + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: simpleble-wheels + path: wheelhouse/*.whl + + - name: Check Packages + run: twine check wheelhouse/*.whl + + - name: Publish packages + if: ${{ env.HAS_TWINE_USERNAME == 'true' }} + run: | + twine upload --repository testpypi --skip-existing wheelhouse/*.whl --verbose + env: + HAS_TWINE_USERNAME: ${{ secrets.TEST_PYPI_USER != '' }} + TWINE_USERNAME: ${{ secrets.TEST_PYPI_USER }} + TWINE_PASSWORD: ${{ secrets.TEST_PYPI_PASSWORD }} diff --git a/third_party/SimpleBLE/.github/workflows/ci_release.yml b/third_party/SimpleBLE/.github/workflows/ci_release.yml deleted file mode 100644 index dca6e3769..000000000 --- a/third_party/SimpleBLE/.github/workflows/ci_release.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: PyPI Release - -on: - workflow_dispatch: - -jobs: - build_wheels: - strategy: - matrix: - os: [ubuntu-20.04 , windows-2019, macos-11] - runs-on: ${{ matrix.os }} - name: Build wheels on ${{ matrix.os }} - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Set up QEMU - if: runner.os == 'Linux' - uses: docker/setup-qemu-action@v1 - with: - platforms: arm64 - - - name: Set up Python 3.9 - uses: actions/setup-python@v2 - with: - python-version: 3.9 - cache: "pip" - - - name: Install dependencies - run: pip install -r simplepyble/requirements.txt - - - name: Build wheel - uses: pypa/cibuildwheel@v2.9.0 - env: - CIBW_BEFORE_ALL_LINUX: "yum update -y && yum group install -y \"Development Tools\" && yum install -y dbus-devel" - CIBW_ARCHS_LINUX: x86_64 i686 aarch64 - CIBW_ARCHS_MACOS: x86_64 universal2 arm64 - CIBW_ARCHS_WINDOWS: AMD64 x86 - CIBW_SKIP: "*musllinux_* pp*" - with: - package-dir: simplepyble - - - name: Upload wheels - uses: actions/upload-artifact@v1 - with: - name: simpleble-wheels - path: wheelhouse - - - name: Check Packages - run: twine check wheelhouse/*.whl - - - name: Publish packages - run: twine upload wheelhouse/*.whl --verbose - env: - TWINE_USERNAME: ${{ secrets.PYPI_USER }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} diff --git a/third_party/SimpleBLE/.github/workflows/ci_release_test.yml b/third_party/SimpleBLE/.github/workflows/ci_release_test.yml deleted file mode 100644 index e9ee22645..000000000 --- a/third_party/SimpleBLE/.github/workflows/ci_release_test.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: PyPI Test Release - -on: [push, pull_request] - -jobs: - build_wheels: - strategy: - matrix: - os: [ubuntu-20.04 , windows-2019, macos-11] - runs-on: ${{ matrix.os }} - name: Build wheels on ${{ matrix.os }} - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Set up QEMU - if: runner.os == 'Linux' - uses: docker/setup-qemu-action@v1 - with: - platforms: arm64 - - - name: Set up Python 3.9 - uses: actions/setup-python@v2 - with: - python-version: 3.9 - cache: "pip" - - - name: Install dependencies - run: pip install -r simplepyble/requirements.txt - - - name: Build wheel - uses: pypa/cibuildwheel@v2.9.0 - env: - CIBW_BUILD: cp39-* # Only build for Python 3.9 - CIBW_BUILD_VERBOSITY: 3 - CIBW_BEFORE_ALL_LINUX: "yum update -y && yum group install -y \"Development Tools\" && yum install -y dbus-devel" - CIBW_ARCHS_LINUX: x86_64 - CIBW_ARCHS_MACOS: arm64 - CIBW_ARCHS_WINDOWS: AMD64 - CIBW_SKIP: "*musllinux_* pp*" - with: - package-dir: simplepyble - - - name: Upload wheels - uses: actions/upload-artifact@v1 - with: - name: simpleble-wheels - path: wheelhouse - - - name: Check Packages - run: twine check wheelhouse/*.whl - - - name: Publish packages - run: twine upload --repository testpypi --skip-existing wheelhouse/*.whl --verbose - env: - TWINE_USERNAME: ${{ secrets.TEST_PYPI_USER }} - TWINE_PASSWORD: ${{ secrets.TEST_PYPI_PASSWORD }} diff --git a/third_party/SimpleBLE/.github/workflows/ci_rust_build.yml b/third_party/SimpleBLE/.github/workflows/ci_rust_build.yml new file mode 100644 index 000000000..474072835 --- /dev/null +++ b/third_party/SimpleBLE/.github/workflows/ci_rust_build.yml @@ -0,0 +1,74 @@ +name: Rust Build + +on: [push, pull_request] + +jobs: + + # ------------------------------------------------------------ + # build-windows: + # runs-on: windows-2022 + + # strategy: + # fail-fast: false + # max-parallel: 4 + # matrix: + # arch: [Win32, x64] + # sdk: ["10.0.19041.0", "10.0.22000.0"] + + # steps: + # - name: Clone Repository + # uses: actions/checkout@v3 + + + # ------------------------------------------------------------ + + build-macos: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + max-parallel: 4 + matrix: + os: [macos-11, macos-12] + arch: [arm64, x86_64] + + steps: + - name: Clone Repository + uses: actions/checkout@v3 + - name: Install CMake + uses: jwlawson/actions-setup-cmake@v1.13 + with: + cmake-version: '3.21' + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + - name: Compile SimpleBLE + run: cargo build + + # ------------------------------------------------------------ + + build-linux: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + max-parallel: 4 + matrix: + os: [ubuntu-20.04, ubuntu-22.04] + + steps: + - name: Clone Repository + uses: actions/checkout@v3 + - name: Install Dependencies + run: | + sudo -H apt-get update -y + sudo -H apt-get install -y libdbus-1-dev + env: + DEBIAN_FRONTEND: noninteractive + - name: Install CMake + uses: jwlawson/actions-setup-cmake@v1.13 + with: + cmake-version: '3.21' + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + - name: Compile SimpleBLE + run: cargo build diff --git a/third_party/SimpleBLE/.github/workflows/ci_test.yml b/third_party/SimpleBLE/.github/workflows/ci_test.yml index c977b7dc4..6cef17acf 100644 --- a/third_party/SimpleBLE/.github/workflows/ci_test.yml +++ b/third_party/SimpleBLE/.github/workflows/ci_test.yml @@ -6,19 +6,13 @@ jobs: # ------------------------------------------------------------ test-windows: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - max-parallel: 4 - matrix: - os: [windows-2019] + runs-on: windows-2022 steps: - name: Clone Repository uses: actions/checkout@v3 - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.0.2 + uses: microsoft/setup-msbuild@v1.1 - name: Clone and build GTest run: | git clone https://github.com/google/googletest @@ -29,7 +23,7 @@ jobs: shell: cmd - name: Compile Tests for Windows x86 run: | - cmake -B %GITHUB_WORKSPACE%\build_test -G "Visual Studio 16 2019" -DCMAKE_SYSTEM_VERSION=10.0.22000.0 -DGTEST_ROOT=%GITHUB_WORKSPACE%\googletest\install -S %GITHUB_WORKSPACE%/simpleble -DSIMPLEBLE_TEST=ON + cmake -B %GITHUB_WORKSPACE%\build_test -G "Visual Studio 17 2022" -DCMAKE_SYSTEM_VERSION=10.0.22000.0 -DGTEST_ROOT=%GITHUB_WORKSPACE%\googletest\install -S %GITHUB_WORKSPACE%/simpleble -DSIMPLEBLE_TEST=ON cmake --build %GITHUB_WORKSPACE%\build_test --config Release --parallel 4 %GITHUB_WORKSPACE%\build_test\bin\Release\simpleble_test.exe shell: cmd @@ -37,21 +31,15 @@ jobs: # ------------------------------------------------------------ test-macos: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - max-parallel: 4 - matrix: - os: [macos-11.0] + runs-on: macos-12 steps: - name: Clone Repository uses: actions/checkout@v3 - name: Setup Cmake - uses: jwlawson/actions-setup-cmake@v1.12 + uses: jwlawson/actions-setup-cmake@v1.13 with: - cmake-version: '3.21.x' + cmake-version: '3.21' - name: Clone and build GTest run: | git clone https://github.com/google/googletest @@ -68,13 +56,7 @@ jobs: # ------------------------------------------------------------ test-linux: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - max-parallel: 4 - matrix: - os: [ubuntu-20.04] + runs-on: ubuntu-22.04 steps: - name: Clone Repository @@ -90,9 +72,9 @@ jobs: run: | echo "DBUS_SESSION_BUS_ADDRESS=$(dbus-daemon --config-file=/usr/share/dbus-1/session.conf --print-address --fork | cut -d, -f1)" >> $GITHUB_ENV - name: Setup Cmake - uses: jwlawson/actions-setup-cmake@v1.12 + uses: jwlawson/actions-setup-cmake@v1.13 with: - cmake-version: '3.21.x' + cmake-version: '3.21' - name: Clone and build GTest run: | git clone https://github.com/google/googletest @@ -149,16 +131,16 @@ jobs: # ------------------------------------------------------------ test-python: - runs-on: "ubuntu-20.04" + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v3 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - name: Set up Python + uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: 3.11 cache: "pip" - name: Install dependencies @@ -166,8 +148,7 @@ jobs: - name: Install SimplePyBLE with Plain flavor run: python setup.py install --plain - working-directory: ./simplepyble - name: Run PyTest run: pytest - working-directory: ./simplepyble/test \ No newline at end of file + working-directory: ./simplepyble/test diff --git a/third_party/SimpleBLE/.gitignore b/third_party/SimpleBLE/.gitignore index b3c94aecc..8c6f07f0e 100644 --- a/third_party/SimpleBLE/.gitignore +++ b/third_party/SimpleBLE/.gitignore @@ -3,11 +3,13 @@ .DS_Store _build _doxygen +_skbuild /build_* -simplepyble/build -simplepyble/dist +/dist __pycache__ *.egg-info wheelhouse + +target diff --git a/third_party/SimpleBLE/.readthedocs.yaml b/third_party/SimpleBLE/.readthedocs.yaml index e013786ea..311ed90b9 100644 --- a/third_party/SimpleBLE/.readthedocs.yaml +++ b/third_party/SimpleBLE/.readthedocs.yaml @@ -21,7 +21,7 @@ build: post_install: - echo "Command run at 'post_install' step" # NOTE: Apparently sphinx_rtfd_theme==1.0.0 is not compatible with Sphinx 5.2 - - pip install breathe==4.34.0 sphinx_rtd_theme==1.0.0 sphinx==5.1.1 + - pip install breathe==4.34.0 sphinx_rtd_theme==1.0.0 sphinx==5.1.1 sphinxcontrib-mermaid==0.9.2 pre_build: - echo "Command run at 'pre_build' step" post_build: diff --git a/third_party/SimpleBLE/CONTRIBUTING.md b/third_party/SimpleBLE/CONTRIBUTING.md new file mode 100644 index 000000000..7cf014549 --- /dev/null +++ b/third_party/SimpleBLE/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# Contributing to SimpleBLE + +This document outlines the process for contributing to the project and provides some guidelines to ensure a smooth collaboration. + +## Contributor License Agreement + +Before we can accept your contribution, you must sign our Contributor License Agreement (CLA). This agreement ensures that the project has the necessary rights to use and distribute your contributions. + +- If you are contributing as an individual, please sign the Individual CLA. +- If you are contributing on behalf of your employer, please have your employer sign the Corporate CLA. + + + +**Please note:** We cannot accept any contributions without a signed CLA. + +## Coding Guidelines + +- Follow the existing code style and conventions used in the project. +- Write clear, readable, and well-documented code. +- Include unit tests for new features or bug fixes where applicable. +- Ensure your code works on all supported platforms (Linux, Windows, macOS, iOS, and Android). + +## Submitting a Pull Request + +1. Ensure you have signed the CLA before submitting your pull request. +2. Provide a clear and detailed description of your changes in the pull request. +3. Reference any related issues in your pull request description. +4. Be prepared to make changes if requested by the maintainers. + +## Reporting Issues + +- Use the GitHub issue tracker to report bugs or suggest features. +- Before creating a new issue, please check if a similar issue already exists. +- Provide as much detail as possible, including steps to reproduce for bugs. + +## Community Guidelines + +- Be respectful and considerate in all interactions. +- Help others if you can. + +## Questions? + +If you have any questions about contributing, feel free to open an issue or contact the team directly. + +Thank you for contributing to SimpleBLE! Your efforts help make this project better for everyone. \ No newline at end of file diff --git a/third_party/SimpleBLE/Cargo.lock b/third_party/SimpleBLE/Cargo.lock new file mode 100644 index 000000000..06cd8f8ec --- /dev/null +++ b/third_party/SimpleBLE/Cargo.lock @@ -0,0 +1,183 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cmake" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c" +dependencies = [ + "cc", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "cxx" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc831ee6a32dd495436e317595e639a587aa9907bef96fe6e6abc290ab6204e9" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94331d54f1b1a8895cd81049f7eaaaef9d05a7dcb4d1fd08bf3ff0806246789d" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dcd35ba14ca9b40d6e4b4b39961f23d835dbb8eed74565ded361d93e1feb8a" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bbeb29798b407ccd82a3324ade1a7286e0d29851475990b612670f6f5124d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "proc-macro2" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + +[[package]] +name = "simplersble" +version = "0.8.0" +dependencies = [ + "cmake", + "cxx", + "cxx-build", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/third_party/SimpleBLE/Cargo.toml b/third_party/SimpleBLE/Cargo.toml new file mode 100644 index 000000000..420afaae0 --- /dev/null +++ b/third_party/SimpleBLE/Cargo.toml @@ -0,0 +1,31 @@ +[package] +edition = "2021" + +name = "simplersble" +version = "0.8.0" +license = "GPL-3.0-only" +description = "The all-in-one Bluetooth library that makes it easy to add wireless connectivity to your projects." +readme = "simplersble/README.md" +repository = "https://github.com/OpenBluetoothToolbox/SimpleBLE" + +include = [ + "/VERSION", + "/cmake", + "/simpleble", + "/simplebluez", + "/simpledbus", + "/simplersble", +] + +build = "simplersble/build.rs" +links = "simpleble" + +[lib] +path = "simplersble/src/lib.rs" + +[dependencies] +cxx = "1.0" + +[build-dependencies] +cmake = "0.1" +cxx-build = "1.0" diff --git a/third_party/SimpleBLE/LICENSE.md b/third_party/SimpleBLE/LICENSE.md index b45662d88..60a363217 100644 --- a/third_party/SimpleBLE/LICENSE.md +++ b/third_party/SimpleBLE/LICENSE.md @@ -1,21 +1,425 @@ -MIT License - -Copyright (c) 2021-2022 Kevin Dewald - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +# SimpleBLE License + +Since February 15th 2024, SimpleBLE is now available under the GNU General Public License +version 3 (GPLv3), with the option for a commercial license without the GPLv3 restrictions +available for a fee. + +**You can find more information on pricing and commercial terms of service at www.simpleble.org** + +To enquire about a commercial license, please contact us at ``contact at simpleble dot org``. + +Likewise, if you are using SimpleBLE in an open-source project and would like to request +a free commercial license or if you have any other questions, please reach out at ``contact at simpleble dot org``. + +------------------------------------------------------------------------------- + +# SimpleBLE Commercial License Agreement + +Version 1.2 - 2024-06-28 + +Copyright © 2024 - THE CALIFORNIA OPEN SOURCE COMPANY LLC + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +## TERMS AND CONDITIONS + +### 0. Definitions + +- **"Licensed Technology"** shall mean the software, documentation, code, materials, and any other intellectual property specified in Appendix A as provided by the Licensor to the Licensee under this Agreement, as well as any updates, modifications, or improvements thereto made by either the Licensor or the Licensee. + +- **"Licensor"** shall mean the individual, corporation, or other legal entity that owns or controls the intellectual property rights in the Licensed Technology and grants the Licensee a license to use the Licensed Technology under the terms and conditions of this Agreement. + +- **"Licensee"** shall mean the individual, corporation, or other legal entity that is granted a license to use the Licensed Technology under the terms and conditions of this Agreement. + +- **"Parties"** shall mean the Licensor and the Licensee collectively, and "Party" shall mean either the Licensor or the Licensee individually, as the context requires. + +- **"Affiliate"** shall mean, with respect to a Party, any entity that directly or indirectly controls, is controlled by, or is under common control with such Party. For purposes of this definition, "control" means the power to direct or cause the direction of the management and policies of an entity, whether through the ownership of voting securities, by contract, or otherwise. + +- **"Employee"** shall mean any individual who is employed by either Party or any of its Affiliates on a full-time, part-time, or temporary basis, and who is subject to the direction and control of the employing Party or Affiliate with respect to the time, place, and manner of their work. The term "Employee" shall not include Contractors. + +- **"Contractor"** shall mean any individual or entity that is not an Employee of either Party or any of its Affiliates, but is engaged by a Party or its Affiliate to perform services on their behalf. Contractors may include, but are not limited to, consultants, advisors, or outsourced service providers. + +- **"Intellectual Property Rights"** means any form of protection afforded anywhere in the world by law to inventions, works, designs, software, trade secrets, confidential information, know-how and other proprietary information and data, including without limitation patents (including re-issues, divisions, continuations and extensions thereof), copyrights, database rights, registered and not-registered design rights, utility models, mask works, as well as applications for any such rights. + +- **"Application"** shall mean a product, service, or solution developed by the Licensee that incorporates the Licensed Technology and adds primary and substantial functionality, features, or improvements that are distinct from those provided by the Licensed Technology itself. An Application must have a clear and distinctive goal that goes beyond merely serving as a wrapper, modification, or adaptation of the Licensed Technology. + +- **"Derivative Work"** shall mean any modification, adaptation, translation, improvement, enhancement, or other work based on or derived from the Licensed Technology that does not add primary and substantial functionality, features, or improvements distinct from those provided by the Licensed Technology itself. This includes, without limitation, any product, service, or solution that integrates, incorporates, bundles, wraps, interfaces with, or adapts the Licensed Technology without significantly changing or enhancing its original functionality, features, or purpose. + +- **"Documentation"** shall mean any user manuals, technical manuals, specifications, or other written materials provided by the Licensor that describe the features, functions, or operation of the Licensed Technology. + +- **"Feedback"** shall mean any suggestions, comments, ideas, or other information provided by the Licensee to the Licensor regarding the Licensed Technology, including but not limited to bug reports, feature requests, and performance evaluations. + +- **"Compatible Open-Source Licenses"** shall mean open-source software licenses allowing for the free use, modification, and distribution of the Licensed Technology, without imposing conflicting or additional restrictions. The only valid licenses are: (a) MIT License (b) Apache License 2.0 (c) BSD 3-Clause "New" or "Revised" License. + +- **"Non-Compatible Open-Source Licenses"** shall mean any license terms that are inconsistent with or conflict with the terms of this Agreement, including but not limited to the GNU General Public License (GPL), Lesser GPL (LGPL), or the Creative Commons Attribution-ShareAlike License. + +- **"Fees"** shall mean the amounts payable by the Licensee to the Licensor in consideration for the rights and licenses granted under this Agreement. + +- **"Term"** shall mean the duration of this Agreement, as specified in Appendix A, commencing on the Effective Date and continuing until the expiration or termination of this Agreement in accordance with its terms. + +- **"Effective Date"** shall mean the date on which this Agreement comes into force, as specified in Appendix A or as determined by the mutual execution of this Agreement by both parties. + +- **"Force Majeure Event"** shall have the meaning set forth in Section 13. + +- **"End User"** shall mean any individual, corporation, or other legal entity that is not a party to this Agreement, but obtains and uses the Application developed by the Licensee, either under the terms of a Compatible License or through a commercial arrangement with the Licensee. + +### 1. Object of the Agreement + +1.1 The primary purpose of this Agreement is to delineate the terms under which the Licensor grants the Licensee specific rights pertaining to the use of the Licensed Technology. It is explicitly stated and understood that this Agreement does not grant any rights, privileges, or licenses to any third parties not explicitly mentioned herein. + +1.2 Under the terms outlined in this Agreement, contingent upon the Licensee's timely payment of Fees as specified in Appendix A, and strict adherence to all other terms and conditions herein, the Licensor grants the Licensee a worldwide, non-exclusive, non-sublicensable, and non-transferable license to use or modify the Licensed Technology, for the sole purposes of creating Application based on the Licensed Technology for and marketing, promoting, licensing, maintaining and supporting such Applications to/for End-User,, subject to the limitations and restrictions set forth in this Agreement. + +1.3 All rights bestowed upon the Licensee under this Agreement are conferred solely through licensing and not by sale. These rights are also circumscribed and governed by the terms of this Agreement. This Agreement does not grant any license or other rights in any intellectual property other than the Licensed Technology. No license or rights will arise under this Agreement by implication, estoppel, or any other means. Any attempt at sublicensing that contradicts the terms of this Agreement will be deemed invalid and without effect. + +1.4 The effective period of this Agreement commences both Parties signing this Agreement and the subsequent payment of the Fee by the Licensee. This initiation is contingent upon these dual conditions being met, marking the start of the Term as indicated in Appendix A and continuing for the Term as indicated in Appendix A. Following the conclusion of this Term, the Agreement may be subject to renewal, based on mutual consent of both Parties and contingent upon the Licensee's payment of any applicable renewal fees, as well as their continued compliance with the terms set forth herein. + +1.5 The Licensee acknowledges and agrees that any use of the Licensed Technology or any Derivative Work by the Licensee outside the scope of this Agreement shall constitute a material breach of this Agreement and may result in the immediate termination of the license granted herein. The Licensee shall promptly notify the Licensor of any unauthorized use or disclosure of the Licensed Technology and take reasonable steps to prevent further breach of this Agreement. + +### 2. Intellectual Property + +2.1 The Licensor retains all right, title, and interest, including all Intellectual Property Rights, in and to the Licensed Technology. This Agreement does not grant the Licensee any ownership rights in the Licensed Technology, but only the limited license rights expressly set forth herein. The Licensed Technology is protected by copyright laws, international copyright treaties, and other Intellectual Property Laws and treaties. Any unauthorized copying, modification, distribution, or use of the Licensed Technology is strictly prohibited and may subject the Licensee to legal action. + +2.2 The Licensee shall own the Intellectual Property Rights in any Application developed by the Licensee, excluding the Licensed Technology and any Derivative Works. The Licensee is hereby granted the privilege to alter, adapt, and modify the Licensed Technology as necessary to meet specific requirements or objectives. Any Derivative Work based on or derived from the Licensed Technology shall be owned by the Licensor and considered part of the Licensed Technology, subject to the terms and conditions of this Agreement. The Licensee shall have no ownership rights in Derivative Works. + +2.3 End Users may use, modify, and distribute the Application in accordance with the terms of the applicable Compatible License or commercial agreement, with the licensee, but shall have no rights to the Licensed Technology except as expressly granted in this Agreement. + +### 3. Fees and Taxes + +3.1 The Licensee shall pay the Licensor the fees specified in Appendix A (the "Fees") in accordance with the payment terms set forth therein. If the Licensee fails to pay the Fees within the agreed payment term, the Licensee shall owe Late Payment Charges as specified in Appendix A. + +3.2 All Fees are non-refundable, and the Licensor shall have no obligation to refund any Fees paid by the Licensee, except as expressly provided in this Agreement. The Licensee shall not be entitled for any reason to any set-off, counter-claim, abatement, or other similar deduction to withhold payment of any amount due to the Licensor. + +3.3 All Fees are exclusive of any applicable taxes, levies, or duties, including but not limited to value-added tax, sales tax, or withholding tax. The Licensee shall be responsible for paying all such taxes, levies, or duties associated with the Fees, except for those based on the Licensor's income. + +3.4 If the Licensor is required to pay or collect any such taxes, levies, or duties on behalf of the Licensee, the amount payable by the Licensee shall be increased to the extent necessary to ensure that the Licensor receives a sum equal to the Fees it would have received had no such deduction or withholding been required. The Licensor shall provide the Licensee with appropriate tax invoices or receipts evidencing the payment of any such taxes, levies, or duties. The Licensee shall furnish the Licensor with any relevant tax exemption certificates or other documentation required to minimize or eliminate any applicable taxes, levies, or duties. + +### 4. General Provisions + +4.1 **Entire Agreement.** This Agreement, together with Appendix A, collectively forms the complete and exclusive terms of the arrangement between the Parties. It supersedes all prior or contemporaneous discussions, representations, contracts, including but not limited to previous License Agreements and similar agreements, as well as proposals, whether written or oral, concerning the subject matters herein. + +4.2 **Severability.** In the event any portion of this Agreement is deemed invalid or unenforceable, such portion shall not limit or otherwise modify or affect any other portion of this Agreement Without limiting the generality of the foregoing, if the scope of any provision contained in this Agreement is too broad to permit enforcement to its fullest extent, such covenant shall be enforced to the maximum extent permitted by law, and the Parties hereby agree that such scope may be judicially modified accordingly. + +4.3 **Modification Waiver.** This Agreement is furnished 'as-is' and no amendments to its terms are permissible. The failure of either Party to enforce any provision of this Agreement may not be deemed a waiver of that or any other provision of this Agreement. + +4.4 **Marketing.** Licensee agrees that the Licensor may use the Licensee's name, trade name, and trademark in the Licensor's marketing materials and on its website, solely for the purpose of identifying the Licensee as a customer of the Licensor. Such use shall be in accordance with the Licensee's brand guidelines and shall not imply any endorsement or affiliation beyond the scope of this Agreement. The Licensor shall promptly cease any such use upon written request from the Licensee. + +4.5 **Compliance with Applicable Laws.** In the course of employing the Licensed Technology pursuant to this Agreement, the Licensee shall ensure compliance with all relevant laws and regulations. The Licensee is prohibited from engaging in the renting, leasing, or similar disposal of the Licensed Technology in any manner that contradicts this Agreement. Additionally, the Licensee must refrain from any misappropriation or unauthorized utilization of other products or services offered by the Licensor. + +4.6 **Trademarks.** The Licensee shall not remove or alter any copyright, trademark, or other proprietary rights notice(s) contained in any portion of the Licensed Technology. Applications must add primary and substantial functionality to the Licensed Technology so as not to compete with the Licensed Technology. + +4.7 **Prohibited Activities.** The Licensee shall not use the Licensed Technology for any unlawful purpose, including without limitation the infringement of any third party's Intellectual Property Right or other proprietary rights. The Licensee agrees that it shall not claim any right to or license (except for the license granted pursuant to Section 1.2 above) with respect to the Licensed Technology nor shall the Licensee contest or assist in contesting the Licensor's right, title and interest in and to such the Licensed Technology. Further, the Licensee shall not partake in activities that contribute to or support claims, whether made by the Licensee or any third party, alleging that the Licensed Technology infringes on any patents. The sale of the Licensed Technology or the establishment of a security interest over it is strictly prohibited. Moreover, the distribution of any product, inclusive of the Licensed Technology, shall be conducted strictly in accordance with the permissions explicitly granted by this Agreement. + +4.8 **Assignment.** The Licensee is barred from assigning or transferring any rights, benefits, or obligations under this Agreement, unless such transfer is part of the sale of its relevant business or assets, or is done with the Licensor's prior written consent, which is not to be unreasonably withheld or delayed. In contrast, the Licensor retains the unencumbered right to assign or transfer any of its rights, benefits, or obligations under this Agreement. + +4.9 **Injunctive Relief.** The Licensee hereby acknowledges that unauthorized disclosure and/or use of any of the Licensed Technology could cause irreparable harm and significant injury to the Licensor that may be difficult to ascertain. Accordingly, the Licensee agrees that, in addition to any other rights and remedies it may have, the Licensor will have the right to seek immediate injunctive relief to enforce the obligations under this Agreement without the necessity of posting a bond or any other security. + +4.10 **No Representation.** The Licensee shall not represent itself as an agent of the Licensor for any purpose, nor give any condition or warranty or make any representation on the Licensor's behalf or commit the Licensor to any contracts. Further, the Licensee shall not make any representations, warranties, guarantees or other commitments with respect to the specifications, features or capabilities of the Licensed Technology or otherwise incur any liability on behalf of the Licensor in any circumstances. + +4.11 **Audits.** The Licensor has the right to audit the Licensee compliance with its obligations under this Agreement. The Licensee shall provide to the Licensor and its personnel, auditors and inspectors such assistance, access and co-operation as they may need and reasonably require. + +4.12 **Cumulative Remedies.** Any remedies specified in this Agreement are cumulative and are not intended to be exclusive of any other remedies to which the Licensor may be entitled at law or equity in case of any breach or threatened breach by the Licensee of any provision of this Agreement, unless such remedies are specifically limited or excluded by the terms of any provision of this Agreement. + +4.13 **Survival.** Certain provisions of this Agreement are designed to outlast its termination. These provisions, due to their inherent nature or as specifically indicated, will continue to be valid and enforceable even after the termination of this Agreement. + +### 5. Feedback and Updates + +5.1 The Licensee hereby grants to the Licensor the unrestricted right to freely utilize and disclose any Feedback that the Licensee provides concerning the Licensed Technology. The Licensee acknowledges and agrees that all Feedback may be employed by the Licensor for any purpose, whether commercial, developmental, or otherwise, without any obligation for acknowledgment, compensation, or other consideration to the Licensee. This includes, but is not limited to, the right to develop, copy, publish, modify, enhance, or otherwise make improvements to the Licensed Technology, at the sole discretion of the Licensor. The Licensee recognizes that all Feedback shall be considered non-confidential and the Licensor is free to use such Feedback without any restriction or obligation of confidentiality. + +5.2 During the Term, the Licensor may develop and release updates, upgrades, or new versions of the Licensed Technology. The Licensee shall have the right to receive and use such updates, upgrades, or new versions, subject to the terms and conditions of this Agreement. + +5.3 The Licensee's right to receive and use updates, upgrades, or new versions shall continue until the termination of this Agreement, as outlined in Section 8. Upon termination, the Licensee shall have no further right to receive any updates, upgrades, or new versions of the Licensed Technology. + +5.4 All updates, upgrades, or new versions of the Licensed Technology provided to the Licensee shall be considered part of the Licensed Technology and subject to the terms and conditions of this Agreement, unless otherwise specified by the Licensor in writing. + +5.5 Notwithstanding the Licensee's right to receive updates, upgrades, or new versions, the Licensor is under no obligation to develop or release any such updates, upgrades, or new versions. The development and release of updates, upgrades, or new versions shall be at the sole discretion of the Licensor. + +### 6. End User Rights and Obligations + +6.1 End Users who obtain the Application, either under a Compatible Open-Source License or through a commercial arrangement with the Licensee, may use, modify, and distribute the Application, subject to the terms and conditions of the applicable Compatible Open-Source License or commercial agreement. However, End Users shall have no right to use, modify, or distribute the Licensed Technology, or any part thereof, except as incorporated in the Application and subject to the terms and conditions of this Agreement. + +6.2 The Licensee shall include a prominent notice in the Application's documentation, user interface, and source code, stating the following: + +``` +IMPORTANT NOTICE: This Application is built upon the [Licensed Technology Name], which is owned by [Licensor Name] and used under license. As an End User of this Application, you are prohibited from using, modifying, or distributing the [Licensed Technology Name], or any part thereof, except as incorporated in this Application. Specifically, you may not: + +a) Extract, isolate, or separate the [Licensed Technology Name] from this Application; +b) Use the [Licensed Technology Name] for any purpose other than as part of this Application; +c) Modify, adapt or translate the [Licensed Technology Name]; +d) Create any Derivative Works based on the [Licensed Technology Name]; +e) Distribute, sublicense, rent, lease, lend, or transfer the [Licensed Technology Name] to any third party; +f) Reverse engineer, decompile, disassemble, or attempt to derive the source code of the [Licensed Technology Name]; +g) Remove, obscure, or alter any copyright, trademark, or other proprietary rights notices contained within the [Licensed Technology Name]. + +Your use of this Application is subject to your compliance with these restrictions. If you have any questions about these terms, please contact [Licensee Name]. +``` + +The Licensee shall include a copy of this notice in all copies or substantial portions of the Application. + +6.3 End Users must comply with all applicable laws, regulations, and third-party rights when using, modifying, or distributing the Application. + +6.4 The Licensee shall indemnify, defend, and hold harmless the Licensor from and against any claims, damages, liabilities, costs, and expenses arising out of or in connection with any use of the Application by End Users, including any use that breaches the terms and conditions of this Agreement. + +### 7. Use of Non-Compatible Open-Source Licenses + +In adherence to the terms of this Agreement, the Licensee is expressly prohibited from, and shall not permit any third party or End User to, integrate, distribute, or otherwise utilize the Licensed Technology in conjunction with any code, software, or content that is subject to a license incompatible with the terms of this Agreement, hereafter referred to as "Non-Compatible Open-Source Licenses". Such prohibitive action includes, but is not limited to, any combination or use that would necessitate, either directly or indirectly, that the Licensed Technology be subject or conform to any licensing terms other than those explicitly set forth in this Agreement. + +### 8. Termination + +8.1 This Agreement may be terminated without cause by either Party upon written notice to the other Party, provided such notice period is no less than three (3) months. + +8.2 Either Party may terminate this Agreement with immediate effect, if the other Party commits a material breach of the terms of this Agreement and has not remedied such breach upon the non-breaching Party's written notice within a reasonable timeframe, which shall be no less than thirty (30) days). + +8.3 The Licensor may also immediately terminate this Agreement upon written notice to the Licensee if the Licensee becomes bankrupt, insolvent, enters into liquidation, or undergoes debt restructuring. + +### 9. Rights and Duties upon Termination + +9.1 Upon termination of this Agreement, the Licensee's rights to use or modify the Licensed Technology, and to create Derivative Works based on the Licensed Technology, shall immediately cease, except as provided in Section 9.2. + +9.2 In the event that this Agreement is terminated by either Party without cause pursuant to Section 8.1, or by the Licensor due to the Licensee's bankruptcy, insolvency, liquidation, or debt restructuring pursuant to Section 8.3, or upon the conclusion of the Term as specified in Section 1.4, the Licensee shall retain the right to continue using the Licensed Technology, limited to the version in use immediately prior to the termination date, solely for the purpose of maintaining and supporting existing Applications. The Licensee shall have no right to receive any updates, modifications, or improvements to the Licensed Technology made by the Licensor after the termination date. + +9.3 In the event that this Agreement is terminated by either Party due to a material breach by the other Party pursuant to Section 8.2, the Licensee's right to continue using the Licensed Technology as described in Section 9.2 shall not apply, and all rights granted to the Licensee under this Agreement shall immediately terminate. + +9.4 Expiry or termination of this Agreement shall not relieve the Licensee of its obligation to pay any Fees accrued or payable to the Licensor prior to the effective date of termination, and the Licensee shall pay to the Licensor all such Fees within 30 days from the effective date of termination. + +9.5 The Licensee acknowledges and agrees that, following the termination of this Agreement, it shall remain bound by its obligations under this Agreement with respect to any use of the Licensed Technology, including in Applications already distributed, and shall ensure that such use remains in compliance with the terms of this Agreement. + +### 10. Notices + +Any notice given by one Party to the other shall be deemed properly given and deemed received if specifically acknowledged by the receiving Party in writing. Additionally, any notice issued by the Licensor to the Licensee using the email address provided by the Licensee to the Licensor will be effective from the moment it was sent. Each communication and document made or delivered by one Party to the other Party pursuant to this Agreement shall be in the language specified in Appendix A. + +### 11. Warranty Disclaimers + +THE LICENSED TECHNOLOGY, INCLUDING ALL SOFTWARE, DOCUMENTATION, INFORMATION, CONTENT, MATERIALS, CODE, AND RELATED SERVICES, ARE PROVIDED BY THE LICENSOR ON AN "AS IS" AND "AS AVAILABLE" BASIS. THE LICENSOR AND ITS AFFILIATES HEREBY DISCLAIM ALL WARRANTIES, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE. SPECIFICALLY, THE LICENSOR AND ITS AFFILIATES MAKE NO REPRESENTATIONS OR WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, REGARDING THE LICENSED TECHNOLOGY, INCLUDING BUT NOT LIMITED TO IMPLIED OR STATUTORY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT OF INTELLECTUAL PROPERTY OR OTHER PROPRIETARY RIGHTS, AND ALL WARRANTIES ARISING FROM COURSE OF DEALING, USAGE, OR TRADE PRACTICE. THE LICENSOR AND ITS AFFILIATES DO NOT WARRANT THAT THE LICENSED TECHNOLOGY, OR ANY PRODUCTS OR RESULTS OF THE USE THEREOF, WILL MEET LICENSEE'S OR ANY OTHER PERSON'S REQUIREMENTS, OPERATE WITHOUT INTERRUPTION, ACHIEVE ANY INTENDED RESULT, BE COMPATIBLE OR WORK WITH ANY SOFTWARE, SYSTEM, OR OTHER SERVICES, OR BE SECURE, ACCURATE, COMPLETE, FREE OF HARMFUL CODE, OR ERROR-FREE AT THE TIME OF THIS AGREEMENT OR AT ANY TIME IN THE FUTURE. FURTHER, THE LICENSOR AND ITS AFFILIATES DO NOT WARRANT THAT THE LICENSED TECHNOLOGY IS FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS. THE PARTIES ACKNOWLEDGE AND AGREE THAT THE FOREGOING WARRANTY DISCLAIMERS ARE AN ESSENTIAL ELEMENT IN SETTING CONSIDERATION UNDER THIS AGREEMENT. + +### 12. Limitation of Liability + +TO THE FULL EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSOR WILL NOT BE LIABLE FOR ANY LOST PROFITS, LOSS OF SAVINGS, LOSS OR CORRUPTION OF DATA, LOSS OR INTERRUPTION OF BUSINESS, LOSS OF GOODWILL OR REPUTATION, CLAIMS MADE BY ANY END-USER OR ANY OTHER THIRD PARTY, OR ANY OTHER INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL, OR PUNITIVE COSTS, DAMAGES, OR EXPENSES OF ANY KIND ARISING UNDER OR IN CONNECTION WITH THIS AGREEMENT. FURTHER, THE LICENSOR'S AGGREGATE LIABILITY UNDER THIS AGREEMENT SHALL NOT EXCEED THE TOTAL AMOUNTS PAID (IF ANY) TO THE LICENSOR UNDER THIS AGREEMENT DURING THE TWELVE (12) MONTHS IMMEDIATELY PRECEDING THE EVENTS GIVING RISE TO THE LIABILITY. SEEKING DAMAGES AS LIMITED BY THIS SECTION SHALL BE THE SOLE AND EXCLUSIVE REMEDY TO THE LICENSEE FOR ANY ACT OR OMISSION OF THE LICENSOR. THESE LIMITATIONS OF LIABILITY AND EXCLUSIONS OF POTENTIAL DAMAGES WERE AN ESSENTIAL ELEMENT IN SETTING CONSIDERATION UNDER THIS AGREEMENT. + +### 13. Force Majeure + +Neither Party shall be liable to the other for any delay or non-performance of its obligations hereunder in the event and to the extent that such delay or non-performance is due to an event beyond a Party's reasonable control and without the fault or negligence of such Party. Such causes may include, but are not limited to, any God, terrorist attack, destruction of all available copies of the Licensed Technology, governmental actions such as changes in law or embargoes, cyberattacks including but not limited to significant DDoS attacks or other cybersecurity breaches that disrupt services, critical infrastructure failure involving essential utilities including but not limited to electricity or internet services, but in every case the failure to perform must be beyond the control and without the fault or negligence of the affected Party and cannot be avoided or circumvented by such party ("Force Majeure Event"). In the occurrence of any such event, the affected Party shall notify the other Party as soon as reasonably possible and shall use all reasonable endeavors to mitigate the effects of the Force Majeure Event. If the Force Majeure Event results in a delay or non-performance of a Party for a period of three (3) months or longer, then either Party shall have the right to terminate this Agreement with immediate effect without any liability (except for the obligations of payment arising prior to the Force Majeure Event) towards the other Party. + +### 14. Indemnification + +Under this Agreement, the Licensee agrees to indemnify, defend, and hold harmless the Licensor, its officers, directors, employees, agents, affiliates, successors, and assigns from any liabilities, damages, judgments, awards, losses, costs, expenses, including reasonable attorney and expert witness fees, arising out of or in connection with any third-party claims, suits, actions, or proceedings. This indemnity covers claims resulting from the Licensee's breach of this Agreement, negligence, willful misconduct, use of the Licensed Technology, or related to the Licensee's products. Excluded from this indemnification are claims alleging that the Licensee's authorized use of unmodified Licensed Technology provided by the Licensor infringes any patent, trademark, or copyright. The Licensor reserves the right, at its option and sole discretion, to assume full control of the defense of such claims with legal counsel of its choice. The Licensee is not permitted to enter into any third-party agreement that would affect the Licensor's rights, constitute an admission of fault by the Licensor, or bind the Licensor in any manner, without the Licensor's prior written consent. The Licensee must promptly notify the Licensor in writing of any claims brought against the Licensor for which indemnification or defense is sought, thereby enabling the Licensor to manage legal strategies and potential settlements effectively. For the avoidance of doubt, the Licensor does not have any obligation to defend the Licensee or any End-User against such claim and therefore such defense shall be up to the Licensor's sole discretion. If the Licensor decides to not defend a claim for infringement of Intellectual Property Rights, the Licensee may defend itself through counsel of its own choice and at its own expense. The Licensor shall have no liability for any costs, expenses, losses or damages incurred by the Licensee or any of its End-Users in the event of such claim. + +### 15. Governing Law and Jurisdiction + +The Licensor and the Licensee hereby mutually concur that the governance and interpretation of this Agreement shall be in strict accordance with the legal statutes and principles as stipulated in Appendix A. + +### 16. Dispute Resolution + +16.1 It is explicitly agreed that any dispute, controversy, or claim that may arise out of, or in connection with, this contract, inclusive of, but not limited to, its formation, interpretation, breach, or termination, and the arbitrability of the claims posited therein, shall be definitively resolved through arbitration. This arbitration shall be conducted in full alignment with the JAMS International Arbitration Rules. + +16.2 Furthermore, the arbitral tribunal shall be comprised of a sole arbitrator. The venue for such arbitration proceedings shall be as delineated in Appendix A of this Agreement. Moreover, the proceedings of the arbitration shall be conducted in the language specified within Appendix A. It is hereby declared that the award rendered by the said arbitrator shall be binding and enforceable, and judgment thereon may be entered in any court of competent jurisdiction. + +### 17. Confidentiality + +17.1 During the term of this Agreement and thereafter until Confidential Information becomes subject to one or more of the exceptions set forth in Section 17.3, each Party shall maintain strict confidentiality regarding the other Party's Confidential Information. "Confidential Information" shall include, but is not limited to, the Licensed Technology, trade secrets, business plans, strategies, customer information, and any other proprietary or sensitive information disclosed by one Party to the other. + +17.2 Each Party agrees to use the Confidential Information solely for the purposes of exercising its rights and fulfilling its obligations under this Agreement. The receiving Party shall not disclose or permit access to Confidential Information to any third party, except to its Employees, Contractors, or agents who need to know such information for the purposes of this Agreement and who are bound by confidentiality obligations no less protective than those set forth herein. + +17.3 The confidentiality obligations shall not apply to information that: (a) is or becomes publicly available through no fault of the receiving Party; (b) was known to the receiving Party prior to disclosure; (c) is independently developed by the receiving Party without use of or reference to the Confidential Information; or (d) is required to be disclosed by law or court order, provided that the receiving Party gives prompt notice to the disclosing Party to enable it to seek a protective order. + +------------------------------------------------------------------------------- + +# GNU General Public License version 3 + +**Preamble** + +The GNU General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program–to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers’ and authors’ protection, the GPL clearly explains that there is no warranty for this free software. For both users’ and authors’ sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users’ freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +**TERMS AND CONDITIONS** +**0. Definitions.** + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the Program. + +To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +**1. Source Code.** + +The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work’s System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +**2. Basic Permissions.** + +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +**3. Protecting Users’ Legal Rights From Anti-Circumvention Law.** + +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work’s users, your or third parties’ legal rights to forbid circumvention of technological measures. + +**4. Conveying Verbatim Copies.** + +You may convey verbatim copies of the Program’s source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +**5. Conveying Modified Source Versions.** + +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + +- a) The work must carry prominent notices stating that you modified it, and giving a relevant date. +- b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. +- c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. +- d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation’s users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +**6. Conveying Non-Source Forms.** + +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + +- a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. +- b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. +- c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. +- d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. +- e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +**7. Additional Terms.** + +“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + +- a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or +- b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or +- c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or +- d) Limiting the use for publicity purposes of names of licensors or authors of the material; or +- e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or +- f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +**8. Termination.** + +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +**9. Acceptance Not Required for Having Copies.** + +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +**10. Automatic Licensing of Downstream Recipients.** + +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party’s predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +**11. Patents.** + +A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor’s “contributor version”. + +A contributor’s “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor’s essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient’s use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +**12. No Surrender of Others’ Freedom.** + +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +**13. Use with the GNU Affero General Public License.** + +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. + +**14. Revised Versions of this License.** + +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy’s public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +**15. Disclaimer of Warranty.** + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +**16. Limitation of Liability.** + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +**17. Interpretation of Sections 15 and 16.** + +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. \ No newline at end of file diff --git a/third_party/SimpleBLE/MANIFEST.in b/third_party/SimpleBLE/MANIFEST.in new file mode 100644 index 000000000..dd35566cb --- /dev/null +++ b/third_party/SimpleBLE/MANIFEST.in @@ -0,0 +1,178 @@ +include LICENSE.md +include VERSION +include cmake/epilogue.cmake +include cmake/find/FindDBus1.cmake +include cmake/find/Findfmt.cmake +include cmake/parse_version.cmake +include cmake/prelude.cmake +include pyproject.toml +include setup.py +include simpleble/CMakeLists.txt +include simpleble/cmake/simpleble.pc.in +include simpleble/include/simpleble/Adapter.h +include simpleble/include/simpleble/AdapterSafe.h +include simpleble/include/simpleble/Characteristic.h +include simpleble/include/simpleble/Descriptor.h +include simpleble/include/simpleble/Exceptions.h +include simpleble/include/simpleble/Logging.h +include simpleble/include/simpleble/Peripheral.h +include simpleble/include/simpleble/PeripheralSafe.h +include simpleble/include/simpleble/Service.h +include simpleble/include/simpleble/SimpleBLE.h +include simpleble/include/simpleble/Types.h +include simpleble/include/simpleble/Utils.h +include simpleble/include/simpleble_c/adapter.h +include simpleble/include/simpleble_c/logging.h +include simpleble/include/simpleble_c/peripheral.h +include simpleble/include/simpleble_c/simpleble.h +include simpleble/include/simpleble_c/types.h +include simpleble/include/simpleble_c/utils.h +include simpleble/src/CommonUtils.h +include simpleble/src/Exceptions.cpp +include simpleble/src/Logging.cpp +include simpleble/src/LoggingInternal.h +include simpleble/src/Utils.cpp +include simpleble/src/backends/common/AdapterBaseTypes.h +include simpleble/src/backends/common/CharacteristicBase.cpp +include simpleble/src/backends/common/CharacteristicBase.h +include simpleble/src/backends/common/DescriptorBase.cpp +include simpleble/src/backends/common/DescriptorBase.h +include simpleble/src/backends/common/ServiceBase.cpp +include simpleble/src/backends/common/ServiceBase.h +include simpleble/src/backends/linux/AdapterBase.cpp +include simpleble/src/backends/linux/AdapterBase.h +include simpleble/src/backends/linux/Bluez.cpp +include simpleble/src/backends/linux/Bluez.h +include simpleble/src/backends/linux/PeripheralBase.cpp +include simpleble/src/backends/linux/PeripheralBase.h +include simpleble/src/backends/macos/AdapterBase.h +include simpleble/src/backends/macos/AdapterBase.mm +include simpleble/src/backends/macos/AdapterBaseMacOS.h +include simpleble/src/backends/macos/AdapterBaseMacOS.mm +include simpleble/src/backends/macos/PeripheralBase.h +include simpleble/src/backends/macos/PeripheralBase.mm +include simpleble/src/backends/macos/PeripheralBaseMacOS.h +include simpleble/src/backends/macos/PeripheralBaseMacOS.mm +include simpleble/src/backends/macos/Utils.h +include simpleble/src/backends/macos/Utils.mm +include simpleble/src/backends/plain/AdapterBase.cpp +include simpleble/src/backends/plain/AdapterBase.h +include simpleble/src/backends/plain/PeripheralBase.cpp +include simpleble/src/backends/plain/PeripheralBase.h +include simpleble/src/backends/windows/AdapterBase.cpp +include simpleble/src/backends/windows/AdapterBase.h +include simpleble/src/backends/windows/PeripheralBase.cpp +include simpleble/src/backends/windows/PeripheralBase.h +include simpleble/src/backends/windows/Utils.cpp +include simpleble/src/backends/windows/Utils.h +include simpleble/src/builders/AdapterBuilder.cpp +include simpleble/src/builders/AdapterBuilder.h +include simpleble/src/builders/CharacteristicBuilder.cpp +include simpleble/src/builders/CharacteristicBuilder.h +include simpleble/src/builders/DescriptorBuilder.cpp +include simpleble/src/builders/DescriptorBuilder.h +include simpleble/src/builders/PeripheralBuilder.cpp +include simpleble/src/builders/PeripheralBuilder.h +include simpleble/src/builders/ServiceBuilder.cpp +include simpleble/src/builders/ServiceBuilder.h +include simpleble/src/external/kvn_safe_callback.hpp +include simpleble/src/external/logfwd.hpp +include simpleble/src/frontends/base/Adapter.cpp +include simpleble/src/frontends/base/Characteristic.cpp +include simpleble/src/frontends/base/Descriptor.cpp +include simpleble/src/frontends/base/Peripheral.cpp +include simpleble/src/frontends/base/Service.cpp +include simpleble/src/frontends/safe/AdapterSafe.cpp +include simpleble/src/frontends/safe/PeripheralSafe.cpp +include simpleble/src_c/adapter.cpp +include simpleble/src_c/logging.cpp +include simpleble/src_c/peripheral.cpp +include simpleble/src_c/simpleble.cpp +include simpleble/src_c/utils.cpp +include simplebluez/CMakeLists.txt +include simplebluez/cmake/simplebluez.pc.in +include simplebluez/include/simplebluez/Adapter.h +include simplebluez/include/simplebluez/Agent.h +include simplebluez/include/simplebluez/Bluez.h +include simplebluez/include/simplebluez/Characteristic.h +include simplebluez/include/simplebluez/Descriptor.h +include simplebluez/include/simplebluez/Device.h +include simplebluez/include/simplebluez/Exceptions.h +include simplebluez/include/simplebluez/ProxyOrg.h +include simplebluez/include/simplebluez/ProxyOrgBluez.h +include simplebluez/include/simplebluez/Service.h +include simplebluez/include/simplebluez/Types.h +include simplebluez/include/simplebluez/interfaces/Adapter1.h +include simplebluez/include/simplebluez/interfaces/Agent1.h +include simplebluez/include/simplebluez/interfaces/AgentManager1.h +include simplebluez/include/simplebluez/interfaces/Battery1.h +include simplebluez/include/simplebluez/interfaces/Device1.h +include simplebluez/include/simplebluez/interfaces/GattCharacteristic1.h +include simplebluez/include/simplebluez/interfaces/GattDescriptor1.h +include simplebluez/include/simplebluez/interfaces/GattService1.h +include simplebluez/src/Adapter.cpp +include simplebluez/src/Agent.cpp +include simplebluez/src/Bluez.cpp +include simplebluez/src/Characteristic.cpp +include simplebluez/src/Descriptor.cpp +include simplebluez/src/Device.cpp +include simplebluez/src/Exceptions.cpp +include simplebluez/src/Logging.cpp +include simplebluez/src/Logging.h +include simplebluez/src/ProxyOrg.cpp +include simplebluez/src/ProxyOrgBluez.cpp +include simplebluez/src/Service.cpp +include simplebluez/src/interfaces/Adapter1.cpp +include simplebluez/src/interfaces/Agent1.cpp +include simplebluez/src/interfaces/AgentManager1.cpp +include simplebluez/src/interfaces/Battery1.cpp +include simplebluez/src/interfaces/Device1.cpp +include simplebluez/src/interfaces/GattCharacteristic1.cpp +include simplebluez/src/interfaces/GattDescriptor1.cpp +include simplebluez/src/interfaces/GattService1.cpp +include simpledbus/CMakeLists.txt +include simpledbus/cmake/simpledbus.pc.in +include simpledbus/include/simpledbus/advanced/Interface.h +include simpledbus/include/simpledbus/advanced/Proxy.h +include simpledbus/include/simpledbus/base/Connection.h +include simpledbus/include/simpledbus/base/Exceptions.h +include simpledbus/include/simpledbus/base/Holder.h +include simpledbus/include/simpledbus/base/Message.h +include simpledbus/include/simpledbus/base/Path.h +include simpledbus/include/simpledbus/external/kvn_safe_callback.hpp +include simpledbus/include/simpledbus/external/logfwd.hpp +include simpledbus/include/simpledbus/interfaces/ObjectManager.h +include simpledbus/src/advanced/Interface.cpp +include simpledbus/src/advanced/Proxy.cpp +include simpledbus/src/base/Connection.cpp +include simpledbus/src/base/Exceptions.cpp +include simpledbus/src/base/Holder.cpp +include simpledbus/src/base/Logging.cpp +include simpledbus/src/base/Logging.h +include simpledbus/src/base/Message.cpp +include simpledbus/src/base/Path.cpp +include simpledbus/src/interfaces/ObjectManager.cpp +include simplepyble/CMakeLists.txt +include simplepyble/README.rst +include simplepyble/requirements.txt +include simplepyble/src/main.cpp +include simplepyble/src/simplepyble/__init__.py +include simplepyble/src/wrap_adapter.cpp +include simplepyble/src/wrap_characteristic.cpp +include simplepyble/src/wrap_descriptor.cpp +include simplepyble/src/wrap_peripheral.cpp +include simplepyble/src/wrap_service.cpp +include simplepyble/src/wrap_types.cpp +prune .github +prune docs +prune examples +prune simpleble/test +prune simplebluez/test +prune simpledbus/test +prune simplersble +prune utils +exclude MANIFEST.in +exclude .clang-format +exclude .gitignore +exclude .readthedocs.yaml +exclude README.rst diff --git a/third_party/SimpleBLE/README.rst b/third_party/SimpleBLE/README.rst index 3262fefce..7022d14ad 100644 --- a/third_party/SimpleBLE/README.rst +++ b/third_party/SimpleBLE/README.rst @@ -9,16 +9,19 @@ Overview -------- The SimpleBLE project aims to provide fully cross-platform BLE libraries and bindings -for Python and C++, designed for simplicity and ease of use with a licencing scheme -chosen to be friendly towards commercial use. All specific operating system quirks -are handled internally to provide a consistent behavior across all platforms. The -libraries also provide first-class support for vendorization of all third-party -dependencies, allowing for easy integration into existing projects. +for C++, Python, Rust and other languages, designed for simplicity and ease of use. +All specific operating system quirks are handled internally to provide a consistent behavior +and API across all platforms. The libraries also provide first-class support for vendorization +of all third-party dependencies, allowing for easy integration into existing projects. -This repository offers the source code for the following related libraries: +**NOTICE: Since February 20, 2024 the license terms of SimpleBLE have changed. Please make sure to read and understand the details below.** + +Below you'll find a list of components that are part of SimpleBLE: * **SimpleBLE:** C++ cross-platform BLE library. -* **SimplePyBLE:** Python bindings for SimpleBLE. See `SimplePyBLE`_ PyPI page for more details. +* **SimplePyBLE:** Python bindings for SimpleBLE. See the `SimplePyBLE`_ PyPI page for more details. +* **SimpleDroidBLE:** Android-specific package following the SimpleBLE API. (Still in Alpha, more to come) +* **SimpleRsBLE:** Rust bindings for SimpleBLE (LEGACY - Big refactor coming soon). See the `SimpleRsBLE`_ Crates.io page for more details. * **SimpleBluez:** C++ abstraction layer for BlueZ over DBus. (Linux only) * **SimpleDBus:** C++ wrapper for libdbus-1 with convenience classes to handle DBus object hierarchies effectively. (Linux only) @@ -26,25 +29,43 @@ If you want to use SimpleBLE and need help. **Please do not hesitate to reach ou * Visit our `ReadTheDocs`_ page. * Join our `Discord`_ server. -* Contact me: ``kevin at dewald dot me`` (Dedicated consulting services available) - -Are you using SimpleBLE on your own project and would like to see it featured here? -Reach out and I'll add a link to it below! +* Contact us: ``contact at simpleble dot org`` +* Visit our `website`_ for more information. Supported platforms ------------------- -=========== ============= =================================== ===== -Windows Linux MacOS iOS -=========== ============= =================================== ===== -Windows 10+ Ubuntu 20.04+ 10.15+ (except 12.0, 12.1 and 12.2) 15.0+ -=========== ============= =================================== ===== + +.. list-table:: + :header-rows: 1 + + * - Platform + - Versions + - Notes + * - Windows + - Windows 10+ + - • WSL does not support Bluetooth. + • Only a single adapter is supported by the OS backend. + * - Linux + - Ubuntu 20.04+ + - • Other distros using Bluez as their Bluetooth backend should work. + * - MacOS + - 10.15+ (Catalina and newer) + - • MacOS 12.0, 12.1, and 12.2 have a bug where the adapter won't return any peripherals after scanning. + • Only a single adapter is supported by the OS backend. + * - iOS + - 15.0+ + - • Older versions of iOS might work but haven't been formally tested. + * - Android (Alpha) + - API 31+ + - • Older APIs are missing certain features of the JVM API that are required by SimpleBLE. Projects using SimpleBLE ------------------------ Don't forget to check out the following projects using SimpleBLE: -* `GDSimpleBLE`_ * `BrainFlow`_ +* `InsideBlue`_ +* `NodeWebBluetooth`_ Contributing ------------ @@ -52,16 +73,71 @@ Pull requests are welcome. For major changes, please open an issue first to disc what you would like to change. License -------- +======= + +Since February 15th 2024, SimpleBLE is now available under the GNU General Public License +version 3 (GPLv3), with the option for a commercial license without the GPLv3 restrictions +available for a fee. + +**You can find more information on pricing and commercial terms of service on our `website`_.** + +For further enquiries, please contact us at ``contact at simpleble dot org``. + +Licensing FAQ +------------- + +I'm already using SimpleBLE. What happens to my project? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Please reach out at ``contact at simpleble dot org`` and we can discuss the specifics of your +situation. It is my intention to make this transition as smooth as possible for existing users, +and I'm open to finding a solution that works for everyone. -All components within this project that have not been bundled from -external creators, are licensed under the terms of the `MIT Licence`_. +If you are using SimpleBLE in an open-source project and would like to request +a free commercial license or if you have any other questions, do not hesitate to reach out. + +Why are you making this change? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +So far, SimpleBLE has been a labor of passion with over 4000 hours invested in multiple iterations. +The decision to transition SimpleBLE to a dual-licensing model is mainly driven by the kind +of products that have been built around it, in particular around notable names in the medical +and industrial sectors, which has been both surprising and encouraging. Providing robust support for +these diverse and critical use cases is a resource-intensive endeavor which can't be achieved on +goodwill alone, especially so when the underlying APIs are also evolving and life having its own +plans. By introducing a commercial license, we're opening a pathway to dedicate more resources to +enhance SimpleBLE. Some of the things on the roadmap include: + +- Bindings into more languages and frameworks. +- Hardware-in-the-loop test infrastructure. +- Offering bounties and revenue sharing with other developers who contribute. +- Providing more comprehensive documentation and tutorials. + +Despite this transition, We remain firmly committed to the open-source philosophy. SimpleBLE was grown +a lot thanks to the feedback of the open-source community, and that foundation will always be a part +of the project. The GPLv3 license option ensures continued accessibility for open-source projects, +and we pledge to actively contribute to and collaborate with the community whenever possible. + +Ultimately, the success of SimpleBLE has been fueled by its open nature, and we believe this +dual-licensing model strengthens that success by enabling both community-driven growth and +targeted enhancements that benefit everyone. + +What does the GPLv3 license imply for my commercial project? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The GPLv3 license ensures that end users have the freedom to run, study, share, and modify the software. +It requires that any modified versions of SimpleBLE, or any software incorporating it, also be +distributed under the GPLv3. Essentially, if your project incorporates SimpleBLE and is distributed, +the entire codebase must be open-source under the GPLv3. + +You can find the full text of the GPLv3 license at https://www.gnu.org/licenses/gpl-3.0.html. .. Links +.. _website: https://simpleble.org + .. _SimplePyBLE: https://pypi.org/project/simplepyble/ -.. _MIT Licence: https://github.com/OpenBluetoothToolbox/SimpleBLE/blob/main/LICENCE.md +.. _SimpleRsBLE: https://crates.io/crates/simplersble .. _Discord: https://discord.gg/N9HqNEcvP3 @@ -73,4 +149,6 @@ external creators, are licensed under the terms of the `MIT Licence`_. .. Other projects using SimpleBLE .. _GDSimpleBLE: https://github.com/jferdelyi/GDSimpleBLE -.. _BrainFlow: https://github.com/brainflow-dev/brainflow \ No newline at end of file +.. _BrainFlow: https://github.com/brainflow-dev/brainflow +.. _InsideBlue: https://github.com/eriklins/InsideBlue-BLE-Tool +.. _NodeWebBluetooth: https://github.com/thegecko/webbluetooth diff --git a/third_party/SimpleBLE/VERSION b/third_party/SimpleBLE/VERSION index a918a2aa1..a3df0a695 100644 --- a/third_party/SimpleBLE/VERSION +++ b/third_party/SimpleBLE/VERSION @@ -1 +1 @@ -0.6.0 +0.8.0 diff --git a/third_party/SimpleBLE/cmake/find/FindDBus1.cmake b/third_party/SimpleBLE/cmake/find/FindDBus1.cmake index f005458bd..b61b16310 100644 --- a/third_party/SimpleBLE/cmake/find/FindDBus1.cmake +++ b/third_party/SimpleBLE/cmake/find/FindDBus1.cmake @@ -17,10 +17,4 @@ if(NOT DBus1_FOUND) set(DBus1_FOUND ${PC_DBUS_FOUND}) - # get_cmake_property(_variableNames VARIABLES) - # list (SORT _variableNames) - # foreach (_variableName ${_variableNames}) - # message(STATUS "${_variableName}=${${_variableName}}") - # endforeach() - endif() diff --git a/third_party/SimpleBLE/cmake/find/Findfmt.cmake b/third_party/SimpleBLE/cmake/find/Findfmt.cmake index ed053dff6..3385f3558 100644 --- a/third_party/SimpleBLE/cmake/find/Findfmt.cmake +++ b/third_party/SimpleBLE/cmake/find/Findfmt.cmake @@ -10,7 +10,7 @@ if(LIBFMT_VENDORIZE) set(LIBFMT_GIT_REPOSITORY "https://github.com/fmtlib/fmt.git") endif() if(NOT LIBFMT_GIT_TAG) - set(LIBFMT_GIT_TAG "b6f4ceaed0a0a24ccf575fab6c56dd50ccf6f1a9") # v8.1.1 + set(LIBFMT_GIT_TAG "a33701196adfad74917046096bf5a2aa0ab0bb50") # v9.1.0 endif() if(NOT LIBFMT_LOCAL_PATH) diff --git a/third_party/SimpleBLE/docs/Doxyfile b/third_party/SimpleBLE/docs/Doxyfile index c01742bb7..0e99c25bc 100644 --- a/third_party/SimpleBLE/docs/Doxyfile +++ b/third_party/SimpleBLE/docs/Doxyfile @@ -908,7 +908,7 @@ WARN_LOGFILE = # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = ../simpleble/include +INPUT = ../simpleble/include ../external/include # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses diff --git a/third_party/SimpleBLE/docs/changelog.rst b/third_party/SimpleBLE/docs/changelog.rst index 20f103783..32ac24827 100644 --- a/third_party/SimpleBLE/docs/changelog.rst +++ b/third_party/SimpleBLE/docs/changelog.rst @@ -5,7 +5,77 @@ All notable changes to this project will be documented in this file. The format is based on `Keep a Changelog`_, and this project adheres to `Semantic Versioning`_. -[0.6.0] - 2022-XX-XX +[0.8.0] - XXXX-XX-XX +-------------------- + +**Added** + +- (Android) Alpha preview of Android support. +- (SimpleDBus) Added templated version of creation and getter functions for Holder class. *(Thanks lorsi96!)* + +**Changed** + +- Implemented standalone ByteArray class derived from `kvn::bytearray`. *(Thanks tlifschitz!)* +- **API CHANGE**: Notify and Indicate callback in C bindings now receive the peripheral handle as the first argument. + +**Fixed** + +- (SimpleBluez) Fixed improper handling of non `org.Bluez.Service1` objects within a `org.bluez.Device1` object. *(Thanks Kober Engineering!)* +- (MacOS) Fixed incorrect storage and retrieval with standard Bluetooth UUIDs inside the peripheral class. *(Thanks TellowKrinkle!)* +- (Python) Fixed incorrect handling of the GIL in certain functions. *(Thanks nomenquis and Medra AI!)* + + +[0.7.X] +-------------------- + +This entire series is dedicated to reviewing and updating the license terms of the project. + + +[0.7.0] - 2024-02-15 +-------------------- + +**Added** + +- Function to query the version of SimpleBLE at runtime. +- (Python) Missing API from SimpleBLE::Characteristic. + +**Changed** + +- (MacOS) Main adapter address is now hardcoded to allow caching based on adapter address. *(Thanks BlissChapman!)* +- (Python) Release GIL when calling ``Peripheral.write_request`` and ``Peripheral.write_command``. +- (MacOS) Rewrote the entire backend. +- (MacOS) OperationFailed exception now contains the error message provided by the OS. + +**Fixed** + +- (MacOS) Remove unnecessary timeout during service discovery. *(Thanks BlissChapman!)* +- (MacOS) Return correct list of devices when scanning. *(Thanks roozmahdavian!)* +- (MacOS) Remove unnecessary timeout during characteristic notification. *(Thanks BlissChapman!)* +- (MacOS) Remove unnecessary timeout during operations on characteristics. +- (Windows) Failed connection attempt would not trigger an exception. *(Thanks eriklins!)* +- (Linux) Use correct UUIDs for advertized services. *(Thanks Symbitic!)* + + +[0.6.1] - 2023-03-14 +-------------------- + +**Added** + +- (Python) Generate source distribution packages. +- (SimpleDBus) Proxy objects keep track of their existence on the DBus object tree. + +**Changed** + +- Bluetooth enabled check was moved into the frontend modules. *(Thanks felixdollack!)* +- (Windows) Use the standard C++ exception handling model. *(Thanks TheFrankyJoe!)* + +**Fixed** + +- CI artifacts for non-standard architectures are now properly built. +- (SimpleBluez) Fixed incorrect handling of invalidated children objects. + + +[0.6.0] - 2023-02-23 -------------------- **Added** @@ -14,22 +84,48 @@ The format is based on `Keep a Changelog`_, and this project adheres to `Semanti - Support for advertized services. - Support for GATT Characteristic properties. - Retrieve the MTU value of an established connection. *(Thanks Marco Cruz!)* +- Peripheral addresses can now be queried for their type. *(Thanks camm73!)* +- Tx Power is decoded from advertising data if available. *(Thanks camm73!)* +- Logger now provides default functions to log to a file or to stdout. +- Support for exposing advertized service data. *(Thanks Symbitic!)* +- (Rust) Preliminary implementation of Rust bindings. - (Windows) Logging of WinRT initialization behavior. - (SimpleBluez) Support for GATT characteristic flags. - (SimpleBluez) Support for GATT characteristic MTUs. *(Thanks Marco Cruz!)* - (SimpleBluez) Support for advertized services. +- (SimpleBluez) Mechanism to select the default DBus bus type during compilation-time. *(Thanks MrMinos!)* **Changed** +- Debug, MinSizeRel and RelWithDebInfo targets now contain their appropriate suffix. *(Thanks kutij!)* +- **API CHANGE**: Log level convention changed from uppercase to capitalizing the first letter. +- Updated ``libfmt`` dependency to version 9.1.0. +- Unused ``libfmt`` targets removed from the build process. - (MacOS) More explicit exception messages. - (MacOS) 16-bit UUIDs are now presented in their 128-bit form. +- (MacOS) Adapter address now swapped for a random UUID. *(Thanks nothingisdead!)* +- (Windows) Reinitialize the WinRT backend if a single-threaded apartment is detected. *(Thanks jferdelyi!)* +- (Windows) Callbacks for indications and notifications are now swapped if one already exists. **Fixed** - Incorrect handling of services and characteristics in the Python examples. *(Thanks Carl-CWX!)* +- Minor potential race condition in the safe callback. +- Compilation-time log levels were not being set correctly. *(Thanks chen3496!)* +- Missing function definition in C-bindings. *(Thanks eriklins!)* +- (Linux) Peripheral would still issue callbacks after deletion. - (MacOS) Increased priority of the dispatch queue to prevent jitter in the incoming data. +- (MacOS) Incorrect listing of advertized services. *(Thanks eriklins & Symbitic!)* - (Windows) Missing peripheral identifier data. *(Thanks eriklins!)* -- (Windows) Multiple initialization of the WinRT backend. +- (Windows) Multiple initializations of the WinRT backend. +- (Windows) Incorrect initialization of the WinRT backend. *(Thanks ChatGPT & Andrey1994!)* +- (Windows) Scan callbacks would continue after scan stopped. +- (Windows) Disconnecting would prevent the user from connecting again. *(Thanks klaff, felixdollack & lairdrt!)* +- (Windows) Uncleared callbacks when unsubscribe is called. +- (Windows) Incorrect handling of non-english locale by MSVC. *(Thanks felixdollack!)* +- (Windows) Disconnection callback would not be triggered on a manual disconnect. *(Thanks crashtua!)* +- (Python) Type returned by ``simplepyble.get_operating_system()`` was not defined. +- (SimpleBluez) Incorrect attempt to operate on an uninitialized DBus connection. *(Thanks jacobbreen25!)* [0.5.0] - 2022-09-25 @@ -102,8 +198,8 @@ The format is based on `Keep a Changelog`_, and this project adheres to `Semanti - Updated Linux implementation to use SimpleBluez v0.5.0. - Added support for Windows SDK 10.0.22000.0 -- Updated libfmt to version 8.1.1. -- Cleaned up dependency management for libfmt and SimpleBluez. +- Updated ``libfmt`` to version 8.1.1. +- Cleaned up dependency management for ``libfmt`` and SimpleBluez. - ``Adapter::get_paired_peripherals`` will return an empty list on Windows and MacOS. - (Linux) **(Experimental)** Exceptions thrown inside the Bluez async thread are now caught to prevent lockups. - ``NotConnected`` exception will be thrown instead of ``OperationFailed`` when peripheral not connected. diff --git a/third_party/SimpleBLE/docs/conf.py b/third_party/SimpleBLE/docs/conf.py index 63c640f68..dcb3b5d72 100644 --- a/third_party/SimpleBLE/docs/conf.py +++ b/third_party/SimpleBLE/docs/conf.py @@ -36,6 +36,7 @@ "sphinx.ext.autodoc", "sphinx.ext.autosummary", "sphinx.ext.napoleon", + 'sphinxcontrib.mermaid', 'breathe' ] @@ -65,6 +66,8 @@ html_static_path = ['_static'] # -- Extension configuration ------------------------------------------------- -breathe_projects = { 'simpleble': '_doxygen/xml' } +breathe_projects = { 'simpleble': '_doxygen/xml', + 'external': '_doxygen/xml' + } autosummary_generate = True diff --git a/third_party/SimpleBLE/docs/extras.rst b/third_party/SimpleBLE/docs/extras.rst index 839da736d..e028efabc 100644 --- a/third_party/SimpleBLE/docs/extras.rst +++ b/third_party/SimpleBLE/docs/extras.rst @@ -1,11 +1,14 @@ -====================== +Extras +====== + Building documentation -====================== +---------------------- + To build documentation for the projects in this repository, you first need to install Sphynx, using the following commands: :: - pip3 install sphinx sphinx_rtd_theme + pip3 install -r docs/requirements.txt Once all dependencies have been installed, HTML documentation can be built by calling the following commands: :: @@ -13,19 +16,25 @@ by calling the following commands: :: cd /docs make html - -================= Release checklist -================= +----------------- Before releasing a new version of the project, the following steps should be performed: +#. Ensure content parity between all readmes and the documentation. + + - ``README.rst`` + - ``LICENSE.md`` + - ``simplepyble/README.rst`` + - ``simplersble/README.md`` + #. Review/update the version number in the following files: - ``VERSION`` + - ``Cargo.toml`` - ``docs/changelog.rst`` - - ``simplepyble/setup.py`` + - ``setup.py`` (Add or remove the ``.devX`` suffix as needed.) #. Commit the changes to the repository. @@ -37,8 +46,14 @@ performed: #. Run the CI job to build and upload the package to PyPI. +#. Run the CI job to build and upload the artifacts to GitHub. + +#. Perform a manual release of the Rust crate to crates.io. + - ``cargo publish`` (Ensure that the version number in ``Cargo.toml`` is correct.) + #. Advance the version number in the following files: - ``VERSION`` + - ``Cargo.toml`` - ``docs/changelog.rst`` - - ``simplepyble/setup.py`` + - ``setup.py`` (Add or remove the ``.devX`` suffix as needed.) diff --git a/third_party/SimpleBLE/docs/index.rst b/third_party/SimpleBLE/docs/index.rst index 4355ea019..36f7a0df9 100644 --- a/third_party/SimpleBLE/docs/index.rst +++ b/third_party/SimpleBLE/docs/index.rst @@ -1,5 +1,5 @@ SimpleBLE -========== +========= The ultimate fully-fledged cross-platform library and bindings for Bluetooth Low Energy (BLE). @@ -15,6 +15,7 @@ Table of Contents :caption: General overview + licensing_faq changelog extras @@ -34,6 +35,12 @@ Table of Contents simplepyble/usage simplepyble/api +.. toctree:: + :maxdepth: 2 + :caption: SimpleDroidBLE + + simpledroidble/usage + .. toctree:: :maxdepth: 2 :caption: SimpleBluez diff --git a/third_party/SimpleBLE/docs/licensing_faq.rst b/third_party/SimpleBLE/docs/licensing_faq.rst new file mode 100644 index 000000000..3194d0f0d --- /dev/null +++ b/third_party/SimpleBLE/docs/licensing_faq.rst @@ -0,0 +1,1387 @@ + +License +======= + +Since February 15th 2024, SimpleBLE is now available under the GNU General Public License +version 3 (GPLv3), with the option for a commercial license without the GPLv3 restrictions +available for a fee. You can find more information on pricing and commercial terms of service +on our `website`_. + +For further enquiries, please contact us at ``contact at simpleble dot org``. + +Likewise, if you are using SimpleBLE in an open-source project and would +like to request a free commercial license or if you have any other +questions, please reach out at ``contact at simpleble dot org``. + +Licensing FAQ +------------- + +I'm already using SimpleBLE. What happens to my project? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Please reach out at ``contact at simpleble dot org`` and we can discuss the specifics of your +situation. It is my intention to make this transition as smooth as possible for existing users, +and I'm open to finding a solution that works for everyone. + +If you are using SimpleBLE in an open-source project and would like to request +a free commercial license or if you have any other questions, do not hesitate to reach out. + +Why are you making this change? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +So far, SimpleBLE has been a labor of passion with over 4000 hours invested in multiple iterations. +The decision to transition SimpleBLE to a dual-licensing model is mainly driven by the kind +of products that have been built around it, in particular around notable names in the medical +and industrial sectors, which has been both surprising and encouraging. Providing robust support for +these diverse and critical use cases is a resource-intensive endeavor which can't be achieved on +goodwill alone, especially so when the underlying APIs are also evolving and life having its own +plans. By introducing a commercial license, we're opening a pathway to dedicate more resources to +enhance SimpleBLE. Some of the things on the roadmap include: + +- Bindings into more languages and frameworks. +- Hardware-in-the-loop test infrastructure. +- Offering bounties and revenue sharing with other developers who contribute. +- Providing more comprehensive documentation and tutorials. + +Despite this transition, We remain firmly committed to the open-source philosophy. SimpleBLE was grown +a lot thanks to the feedback of the open-source community, and that foundation will always be a part +of the project. The GPLv3 license option ensures continued accessibility for open-source projects, +and we pledge to actively contribute to and collaborate with the community whenever possible. + +Ultimately, the success of SimpleBLE has been fueled by its open nature, and we believe this +dual-licensing model strengthens that success by enabling both community-driven growth and +targeted enhancements that benefit everyone. + +What does the GPLv3 license imply for my commercial project? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The GPLv3 license ensures that end users have the freedom to run, study, share, and modify the software. +It requires that any modified versions of SimpleBLE, or any software incorporating it, also be +distributed under the GPLv3. Essentially, if your project incorporates SimpleBLE and is distributed, +the entire codebase must be open-source under the GPLv3. + +You can find the full text of the GPLv3 license at https://www.gnu.org/licenses/gpl-3.0.html. + + +-------------- + +SimpleBLE Commercial License Agreement +-------------------------------------- + +Version 1.2 - 2024-06-28 + +Copyright © 2024 - THE CALIFORNIA OPEN SOURCE COMPANY LLC + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +0. Definitions +^^^^^^^^^^^^^^ + +- **“Licensed Technology”** shall mean the software, documentation, + code, materials, and any other intellectual property specified in + Appendix A as provided by the Licensor to the Licensee under this + Agreement, as well as any updates, modifications, or improvements + thereto made by either the Licensor or the Licensee. + +- **“Licensor”** shall mean the individual, corporation, or other legal + entity that owns or controls the intellectual property rights in the + Licensed Technology and grants the Licensee a license to use the + Licensed Technology under the terms and conditions of this Agreement. + +- **“Licensee”** shall mean the individual, corporation, or other legal + entity that is granted a license to use the Licensed Technology under + the terms and conditions of this Agreement. + +- **“Parties”** shall mean the Licensor and the Licensee collectively, + and “Party” shall mean either the Licensor or the Licensee + individually, as the context requires. + +- **“Affiliate”** shall mean, with respect to a Party, any entity that + directly or indirectly controls, is controlled by, or is under common + control with such Party. For purposes of this definition, “control” + means the power to direct or cause the direction of the management + and policies of an entity, whether through the ownership of voting + securities, by contract, or otherwise. + +- **“Employee”** shall mean any individual who is employed by either + Party or any of its Affiliates on a full-time, part-time, or + temporary basis, and who is subject to the direction and control of + the employing Party or Affiliate with respect to the time, place, and + manner of their work. The term “Employee” shall not include + Contractors. + +- **“Contractor”** shall mean any individual or entity that is not an + Employee of either Party or any of its Affiliates, but is engaged by + a Party or its Affiliate to perform services on their behalf. + Contractors may include, but are not limited to, consultants, + advisors, or outsourced service providers. + +- **“Intellectual Property Rights”** means any form of protection + afforded anywhere in the world by law to inventions, works, designs, + software, trade secrets, confidential information, know-how and other + proprietary information and data, including without limitation + patents (including re-issues, divisions, continuations and extensions + thereof), copyrights, database rights, registered and not-registered + design rights, utility models, mask works, as well as applications + for any such rights. + +- **“Application”** shall mean a product, service, or solution + developed by the Licensee that incorporates the Licensed Technology + and adds primary and substantial functionality, features, or + improvements that are distinct from those provided by the Licensed + Technology itself. An Application must have a clear and distinctive + goal that goes beyond merely serving as a wrapper, modification, or + adaptation of the Licensed Technology. + +- **“Derivative Work”** shall mean any modification, adaptation, + translation, improvement, enhancement, or other work based on or + derived from the Licensed Technology that does not add primary and + substantial functionality, features, or improvements distinct from + those provided by the Licensed Technology itself. This includes, + without limitation, any product, service, or solution that + integrates, incorporates, bundles, wraps, interfaces with, or adapts + the Licensed Technology without significantly changing or enhancing + its original functionality, features, or purpose. + +- **“Documentation”** shall mean any user manuals, technical manuals, + specifications, or other written materials provided by the Licensor + that describe the features, functions, or operation of the Licensed + Technology. + +- **“Feedback”** shall mean any suggestions, comments, ideas, or other + information provided by the Licensee to the Licensor regarding the + Licensed Technology, including but not limited to bug reports, + feature requests, and performance evaluations. + +- **“Compatible Open-Source Licenses”** shall mean open-source software + licenses allowing for the free use, modification, and distribution of + the Licensed Technology, without imposing conflicting or additional + restrictions. The only valid licenses are: (a) MIT License (b) Apache + License 2.0 (c) BSD 3-Clause “New” or “Revised” License. + +- **“Non-Compatible Open-Source Licenses”** shall mean any license + terms that are inconsistent with or conflict with the terms of this + Agreement, including but not limited to the GNU General Public + License (GPL), Lesser GPL (LGPL), or the Creative Commons + Attribution-ShareAlike License. + +- **“Fees”** shall mean the amounts payable by the Licensee to the + Licensor in consideration for the rights and licenses granted under + this Agreement. + +- **“Term”** shall mean the duration of this Agreement, as specified in + Appendix A, commencing on the Effective Date and continuing until the + expiration or termination of this Agreement in accordance with its + terms. + +- **“Effective Date”** shall mean the date on which this Agreement + comes into force, as specified in Appendix A or as determined by the + mutual execution of this Agreement by both parties. + +- **“Force Majeure Event”** shall have the meaning set forth in Section + 13. + +- **“End User”** shall mean any individual, corporation, or other legal + entity that is not a party to this Agreement, but obtains and uses + the Application developed by the Licensee, either under the terms of + a Compatible License or through a commercial arrangement with the + Licensee. + +1. Object of the Agreement +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +1.1 The primary purpose of this Agreement is to delineate the terms +under which the Licensor grants the Licensee specific rights pertaining +to the use of the Licensed Technology. It is explicitly stated and +understood that this Agreement does not grant any rights, privileges, or +licenses to any third parties not explicitly mentioned herein. + +1.2 Under the terms outlined in this Agreement, contingent upon the +Licensee’s timely payment of Fees as specified in Appendix A, and strict +adherence to all other terms and conditions herein, the Licensor grants +the Licensee a worldwide, non-exclusive, non-sublicensable, and +non-transferable license to use or modify the Licensed Technology, for +the sole purposes of creating Application based on the Licensed +Technology for and marketing, promoting, licensing, maintaining and +supporting such Applications to/for End-User,, subject to the +limitations and restrictions set forth in this Agreement. + +1.3 All rights bestowed upon the Licensee under this Agreement are +conferred solely through licensing and not by sale. These rights are +also circumscribed and governed by the terms of this Agreement. This +Agreement does not grant any license or other rights in any intellectual +property other than the Licensed Technology. No license or rights will +arise under this Agreement by implication, estoppel, or any other means. +Any attempt at sublicensing that contradicts the terms of this Agreement +will be deemed invalid and without effect. + +1.4 The effective period of this Agreement commences both Parties +signing this Agreement and the subsequent payment of the Fee by the +Licensee. This initiation is contingent upon these dual conditions being +met, marking the start of the Term as indicated in Appendix A and +continuing for the Term as indicated in Appendix A. Following the +conclusion of this Term, the Agreement may be subject to renewal, based +on mutual consent of both Parties and contingent upon the Licensee’s +payment of any applicable renewal fees, as well as their continued +compliance with the terms set forth herein. + +1.5 The Licensee acknowledges and agrees that any use of the Licensed +Technology or any Derivative Work by the Licensee outside the scope of +this Agreement shall constitute a material breach of this Agreement and +may result in the immediate termination of the license granted herein. +The Licensee shall promptly notify the Licensor of any unauthorized use +or disclosure of the Licensed Technology and take reasonable steps to +prevent further breach of this Agreement. + +2. Intellectual Property +^^^^^^^^^^^^^^^^^^^^^^^^ + +2.1 The Licensor retains all right, title, and interest, including all +Intellectual Property Rights, in and to the Licensed Technology. This +Agreement does not grant the Licensee any ownership rights in the +Licensed Technology, but only the limited license rights expressly set +forth herein. The Licensed Technology is protected by copyright laws, +international copyright treaties, and other Intellectual Property Laws +and treaties. Any unauthorized copying, modification, distribution, or +use of the Licensed Technology is strictly prohibited and may subject +the Licensee to legal action. + +2.2 The Licensee shall own the Intellectual Property Rights in any +Application developed by the Licensee, excluding the Licensed Technology +and any Derivative Works. The Licensee is hereby granted the privilege +to alter, adapt, and modify the Licensed Technology as necessary to meet +specific requirements or objectives. Any Derivative Work based on or +derived from the Licensed Technology shall be owned by the Licensor and +considered part of the Licensed Technology, subject to the terms and +conditions of this Agreement. The Licensee shall have no ownership +rights in Derivative Works. + +2.3 End Users may use, modify, and distribute the Application in +accordance with the terms of the applicable Compatible License or +commercial agreement, with the licensee, but shall have no rights to the +Licensed Technology except as expressly granted in this Agreement. + +3. Fees and Taxes +^^^^^^^^^^^^^^^^^ + +3.1 The Licensee shall pay the Licensor the fees specified in Appendix A +(the “Fees”) in accordance with the payment terms set forth therein. If +the Licensee fails to pay the Fees within the agreed payment term, the +Licensee shall owe Late Payment Charges as specified in Appendix A. + +3.2 All Fees are non-refundable, and the Licensor shall have no +obligation to refund any Fees paid by the Licensee, except as expressly +provided in this Agreement. The Licensee shall not be entitled for any +reason to any set-off, counter-claim, abatement, or other similar +deduction to withhold payment of any amount due to the Licensor. + +3.3 All Fees are exclusive of any applicable taxes, levies, or duties, +including but not limited to value-added tax, sales tax, or withholding +tax. The Licensee shall be responsible for paying all such taxes, +levies, or duties associated with the Fees, except for those based on +the Licensor’s income. + +3.4 If the Licensor is required to pay or collect any such taxes, +levies, or duties on behalf of the Licensee, the amount payable by the +Licensee shall be increased to the extent necessary to ensure that the +Licensor receives a sum equal to the Fees it would have received had no +such deduction or withholding been required. The Licensor shall provide +the Licensee with appropriate tax invoices or receipts evidencing the +payment of any such taxes, levies, or duties. The Licensee shall furnish +the Licensor with any relevant tax exemption certificates or other +documentation required to minimize or eliminate any applicable taxes, +levies, or duties. + +4. General Provisions +^^^^^^^^^^^^^^^^^^^^^ + +4.1 **Entire Agreement.** This Agreement, together with Appendix A, +collectively forms the complete and exclusive terms of the arrangement +between the Parties. It supersedes all prior or contemporaneous +discussions, representations, contracts, including but not limited to +previous License Agreements and similar agreements, as well as +proposals, whether written or oral, concerning the subject matters +herein. + +4.2 **Severability.** In the event any portion of this Agreement is +deemed invalid or unenforceable, such portion shall not limit or +otherwise modify or affect any other portion of this Agreement Without +limiting the generality of the foregoing, if the scope of any provision +contained in this Agreement is too broad to permit enforcement to its +fullest extent, such covenant shall be enforced to the maximum extent +permitted by law, and the Parties hereby agree that such scope may be +judicially modified accordingly. + +4.3 **Modification Waiver.** This Agreement is furnished ‘as-is’ and no +amendments to its terms are permissible. The failure of either Party to +enforce any provision of this Agreement may not be deemed a waiver of +that or any other provision of this Agreement. + +4.4 **Marketing.** Licensee agrees that the Licensor may use the +Licensee’s name, trade name, and trademark in the Licensor’s marketing +materials and on its website, solely for the purpose of identifying the +Licensee as a customer of the Licensor. Such use shall be in accordance +with the Licensee’s brand guidelines and shall not imply any endorsement +or affiliation beyond the scope of this Agreement. The Licensor shall +promptly cease any such use upon written request from the Licensee. + +4.5 **Compliance with Applicable Laws.** In the course of employing the +Licensed Technology pursuant to this Agreement, the Licensee shall +ensure compliance with all relevant laws and regulations. The Licensee +is prohibited from engaging in the renting, leasing, or similar disposal +of the Licensed Technology in any manner that contradicts this +Agreement. Additionally, the Licensee must refrain from any +misappropriation or unauthorized utilization of other products or +services offered by the Licensor. + +4.6 **Trademarks.** The Licensee shall not remove or alter any +copyright, trademark, or other proprietary rights notice(s) contained in +any portion of the Licensed Technology. Applications must add primary +and substantial functionality to the Licensed Technology so as not to +compete with the Licensed Technology. + +4.7 **Prohibited Activities.** The Licensee shall not use the Licensed +Technology for any unlawful purpose, including without limitation the +infringement of any third party’s Intellectual Property Right or other +proprietary rights. The Licensee agrees that it shall not claim any +right to or license (except for the license granted pursuant to Section +1.2 above) with respect to the Licensed Technology nor shall the +Licensee contest or assist in contesting the Licensor’s right, title and +interest in and to such the Licensed Technology. Further, the Licensee +shall not partake in activities that contribute to or support claims, +whether made by the Licensee or any third party, alleging that the +Licensed Technology infringes on any patents. The sale of the Licensed +Technology or the establishment of a security interest over it is +strictly prohibited. Moreover, the distribution of any product, +inclusive of the Licensed Technology, shall be conducted strictly in +accordance with the permissions explicitly granted by this Agreement. + +4.8 **Assignment.** The Licensee is barred from assigning or +transferring any rights, benefits, or obligations under this Agreement, +unless such transfer is part of the sale of its relevant business or +assets, or is done with the Licensor’s prior written consent, which is +not to be unreasonably withheld or delayed. In contrast, the Licensor +retains the unencumbered right to assign or transfer any of its rights, +benefits, or obligations under this Agreement. + +4.9 **Injunctive Relief.** The Licensee hereby acknowledges that +unauthorized disclosure and/or use of any of the Licensed Technology +could cause irreparable harm and significant injury to the Licensor that +may be difficult to ascertain. Accordingly, the Licensee agrees that, in +addition to any other rights and remedies it may have, the Licensor will +have the right to seek immediate injunctive relief to enforce the +obligations under this Agreement without the necessity of posting a bond +or any other security. + +4.10 **No Representation.** The Licensee shall not represent itself as +an agent of the Licensor for any purpose, nor give any condition or +warranty or make any representation on the Licensor’s behalf or commit +the Licensor to any contracts. Further, the Licensee shall not make any +representations, warranties, guarantees or other commitments with +respect to the specifications, features or capabilities of the Licensed +Technology or otherwise incur any liability on behalf of the Licensor in +any circumstances. + +4.11 **Audits.** The Licensor has the right to audit the Licensee +compliance with its obligations under this Agreement. The Licensee shall +provide to the Licensor and its personnel, auditors and inspectors such +assistance, access and co-operation as they may need and reasonably +require. + +4.12 **Cumulative Remedies.** Any remedies specified in this Agreement +are cumulative and are not intended to be exclusive of any other +remedies to which the Licensor may be entitled at law or equity in case +of any breach or threatened breach by the Licensee of any provision of +this Agreement, unless such remedies are specifically limited or +excluded by the terms of any provision of this Agreement. + +4.13 **Survival.** Certain provisions of this Agreement are designed to +outlast its termination. These provisions, due to their inherent nature +or as specifically indicated, will continue to be valid and enforceable +even after the termination of this Agreement. + +5. Feedback and Updates +^^^^^^^^^^^^^^^^^^^^^^^ + +5.1 The Licensee hereby grants to the Licensor the unrestricted right to +freely utilize and disclose any Feedback that the Licensee provides +concerning the Licensed Technology. The Licensee acknowledges and agrees +that all Feedback may be employed by the Licensor for any purpose, +whether commercial, developmental, or otherwise, without any obligation +for acknowledgment, compensation, or other consideration to the +Licensee. This includes, but is not limited to, the right to develop, +copy, publish, modify, enhance, or otherwise make improvements to the +Licensed Technology, at the sole discretion of the Licensor. The +Licensee recognizes that all Feedback shall be considered +non-confidential and the Licensor is free to use such Feedback without +any restriction or obligation of confidentiality. + +5.2 During the Term, the Licensor may develop and release updates, +upgrades, or new versions of the Licensed Technology. The Licensee shall +have the right to receive and use such updates, upgrades, or new +versions, subject to the terms and conditions of this Agreement. + +5.3 The Licensee’s right to receive and use updates, upgrades, or new +versions shall continue until the termination of this Agreement, as +outlined in Section 8. Upon termination, the Licensee shall have no +further right to receive any updates, upgrades, or new versions of the +Licensed Technology. + +5.4 All updates, upgrades, or new versions of the Licensed Technology +provided to the Licensee shall be considered part of the Licensed +Technology and subject to the terms and conditions of this Agreement, +unless otherwise specified by the Licensor in writing. + +5.5 Notwithstanding the Licensee’s right to receive updates, upgrades, +or new versions, the Licensor is under no obligation to develop or +release any such updates, upgrades, or new versions. The development and +release of updates, upgrades, or new versions shall be at the sole +discretion of the Licensor. + +6. End User Rights and Obligations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +6.1 End Users who obtain the Application, either under a Compatible +Open-Source License or through a commercial arrangement with the +Licensee, may use, modify, and distribute the Application, subject to +the terms and conditions of the applicable Compatible Open-Source +License or commercial agreement. However, End Users shall have no right +to use, modify, or distribute the Licensed Technology, or any part +thereof, except as incorporated in the Application and subject to the +terms and conditions of this Agreement. + +6.2 The Licensee shall include a prominent notice in the Application’s +documentation, user interface, and source code, stating the following: + +:: + + IMPORTANT NOTICE: This Application is built upon the [Licensed Technology Name], which is owned by [Licensor Name] and used under license. As an End User of this Application, you are prohibited from using, modifying, or distributing the [Licensed Technology Name], or any part thereof, except as incorporated in this Application. Specifically, you may not: + + a) Extract, isolate, or separate the [Licensed Technology Name] from this Application; + b) Use the [Licensed Technology Name] for any purpose other than as part of this Application; + c) Modify, adapt or translate the [Licensed Technology Name]; + d) Create any Derivative Works based on the [Licensed Technology Name]; + e) Distribute, sublicense, rent, lease, lend, or transfer the [Licensed Technology Name] to any third party; + f) Reverse engineer, decompile, disassemble, or attempt to derive the source code of the [Licensed Technology Name]; + g) Remove, obscure, or alter any copyright, trademark, or other proprietary rights notices contained within the [Licensed Technology Name]. + + Your use of this Application is subject to your compliance with these restrictions. If you have any questions about these terms, please contact [Licensee Name]. + +The Licensee shall include a copy of this notice in all copies or +substantial portions of the Application. + +6.3 End Users must comply with all applicable laws, regulations, and +third-party rights when using, modifying, or distributing the +Application. + +6.4 The Licensee shall indemnify, defend, and hold harmless the Licensor +from and against any claims, damages, liabilities, costs, and expenses +arising out of or in connection with any use of the Application by End +Users, including any use that breaches the terms and conditions of this +Agreement. + +7. Use of Non-Compatible Open-Source Licenses +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In adherence to the terms of this Agreement, the Licensee is expressly +prohibited from, and shall not permit any third party or End User to, +integrate, distribute, or otherwise utilize the Licensed Technology in +conjunction with any code, software, or content that is subject to a +license incompatible with the terms of this Agreement, hereafter +referred to as “Non-Compatible Open-Source Licenses”. Such prohibitive +action includes, but is not limited to, any combination or use that +would necessitate, either directly or indirectly, that the Licensed +Technology be subject or conform to any licensing terms other than those +explicitly set forth in this Agreement. + +8. Termination +^^^^^^^^^^^^^^ + +8.1 This Agreement may be terminated without cause by either Party upon +written notice to the other Party, provided such notice period is no +less than three (3) months. + +8.2 Either Party may terminate this Agreement with immediate effect, if +the other Party commits a material breach of the terms of this Agreement +and has not remedied such breach upon the non-breaching Party’s written +notice within a reasonable timeframe, which shall be no less than thirty +(30) days). + +8.3 The Licensor may also immediately terminate this Agreement upon +written notice to the Licensee if the Licensee becomes bankrupt, +insolvent, enters into liquidation, or undergoes debt restructuring. + +9. Rights and Duties upon Termination +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +9.1 Upon termination of this Agreement, the Licensee’s rights to use or +modify the Licensed Technology, and to create Derivative Works based on +the Licensed Technology, shall immediately cease, except as provided in +Section 9.2. + +9.2 In the event that this Agreement is terminated by either Party +without cause pursuant to Section 8.1, or by the Licensor due to the +Licensee’s bankruptcy, insolvency, liquidation, or debt restructuring +pursuant to Section 8.3, or upon the conclusion of the Term as specified +in Section 1.4, the Licensee shall retain the right to continue using +the Licensed Technology, limited to the version in use immediately prior +to the termination date, solely for the purpose of maintaining and +supporting existing Applications. The Licensee shall have no right to +receive any updates, modifications, or improvements to the Licensed +Technology made by the Licensor after the termination date. + +9.3 In the event that this Agreement is terminated by either Party due +to a material breach by the other Party pursuant to Section 8.2, the +Licensee’s right to continue using the Licensed Technology as described +in Section 9.2 shall not apply, and all rights granted to the Licensee +under this Agreement shall immediately terminate. + +9.4 Expiry or termination of this Agreement shall not relieve the +Licensee of its obligation to pay any Fees accrued or payable to the +Licensor prior to the effective date of termination, and the Licensee +shall pay to the Licensor all such Fees within 30 days from the +effective date of termination. + +9.5 The Licensee acknowledges and agrees that, following the termination +of this Agreement, it shall remain bound by its obligations under this +Agreement with respect to any use of the Licensed Technology, including +in Applications already distributed, and shall ensure that such use +remains in compliance with the terms of this Agreement. + +10. Notices +^^^^^^^^^^^ + +Any notice given by one Party to the other shall be deemed properly +given and deemed received if specifically acknowledged by the receiving +Party in writing. Additionally, any notice issued by the Licensor to the +Licensee using the email address provided by the Licensee to the +Licensor will be effective from the moment it was sent. Each +communication and document made or delivered by one Party to the other +Party pursuant to this Agreement shall be in the language specified in +Appendix A. + +11. Warranty Disclaimers +^^^^^^^^^^^^^^^^^^^^^^^^ + +THE LICENSED TECHNOLOGY, INCLUDING ALL SOFTWARE, DOCUMENTATION, +INFORMATION, CONTENT, MATERIALS, CODE, AND RELATED SERVICES, ARE +PROVIDED BY THE LICENSOR ON AN “AS IS” AND “AS AVAILABLE” BASIS. THE +LICENSOR AND ITS AFFILIATES HEREBY DISCLAIM ALL WARRANTIES, WHETHER +EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE. SPECIFICALLY, THE LICENSOR +AND ITS AFFILIATES MAKE NO REPRESENTATIONS OR WARRANTIES OF ANY KIND, +EXPRESS OR IMPLIED, REGARDING THE LICENSED TECHNOLOGY, INCLUDING BUT NOT +LIMITED TO IMPLIED OR STATUTORY WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT OF INTELLECTUAL +PROPERTY OR OTHER PROPRIETARY RIGHTS, AND ALL WARRANTIES ARISING FROM +COURSE OF DEALING, USAGE, OR TRADE PRACTICE. THE LICENSOR AND ITS +AFFILIATES DO NOT WARRANT THAT THE LICENSED TECHNOLOGY, OR ANY PRODUCTS +OR RESULTS OF THE USE THEREOF, WILL MEET LICENSEE’S OR ANY OTHER +PERSON’S REQUIREMENTS, OPERATE WITHOUT INTERRUPTION, ACHIEVE ANY +INTENDED RESULT, BE COMPATIBLE OR WORK WITH ANY SOFTWARE, SYSTEM, OR +OTHER SERVICES, OR BE SECURE, ACCURATE, COMPLETE, FREE OF HARMFUL CODE, +OR ERROR-FREE AT THE TIME OF THIS AGREEMENT OR AT ANY TIME IN THE +FUTURE. FURTHER, THE LICENSOR AND ITS AFFILIATES DO NOT WARRANT THAT THE +LICENSED TECHNOLOGY IS FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS. THE +PARTIES ACKNOWLEDGE AND AGREE THAT THE FOREGOING WARRANTY DISCLAIMERS +ARE AN ESSENTIAL ELEMENT IN SETTING CONSIDERATION UNDER THIS AGREEMENT. + +12. Limitation of Liability +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +TO THE FULL EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSOR WILL NOT BE +LIABLE FOR ANY LOST PROFITS, LOSS OF SAVINGS, LOSS OR CORRUPTION OF +DATA, LOSS OR INTERRUPTION OF BUSINESS, LOSS OF GOODWILL OR REPUTATION, +CLAIMS MADE BY ANY END-USER OR ANY OTHER THIRD PARTY, OR ANY OTHER +INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL, OR PUNITIVE COSTS, +DAMAGES, OR EXPENSES OF ANY KIND ARISING UNDER OR IN CONNECTION WITH +THIS AGREEMENT. FURTHER, THE LICENSOR’S AGGREGATE LIABILITY UNDER THIS +AGREEMENT SHALL NOT EXCEED THE TOTAL AMOUNTS PAID (IF ANY) TO THE +LICENSOR UNDER THIS AGREEMENT DURING THE TWELVE (12) MONTHS IMMEDIATELY +PRECEDING THE EVENTS GIVING RISE TO THE LIABILITY. SEEKING DAMAGES AS +LIMITED BY THIS SECTION SHALL BE THE SOLE AND EXCLUSIVE REMEDY TO THE +LICENSEE FOR ANY ACT OR OMISSION OF THE LICENSOR. THESE LIMITATIONS OF +LIABILITY AND EXCLUSIONS OF POTENTIAL DAMAGES WERE AN ESSENTIAL ELEMENT +IN SETTING CONSIDERATION UNDER THIS AGREEMENT. + +13. Force Majeure +^^^^^^^^^^^^^^^^^ + +Neither Party shall be liable to the other for any delay or +non-performance of its obligations hereunder in the event and to the +extent that such delay or non-performance is due to an event beyond a +Party’s reasonable control and without the fault or negligence of such +Party. Such causes may include, but are not limited to, any God, +terrorist attack, destruction of all available copies of the Licensed +Technology, governmental actions such as changes in law or embargoes, +cyberattacks including but not limited to significant DDoS attacks or +other cybersecurity breaches that disrupt services, critical +infrastructure failure involving essential utilities including but not +limited to electricity or internet services, but in every case the +failure to perform must be beyond the control and without the fault or +negligence of the affected Party and cannot be avoided or circumvented +by such party (“Force Majeure Event”). In the occurrence of any such +event, the affected Party shall notify the other Party as soon as +reasonably possible and shall use all reasonable endeavors to mitigate +the effects of the Force Majeure Event. If the Force Majeure Event +results in a delay or non-performance of a Party for a period of three +(3) months or longer, then either Party shall have the right to +terminate this Agreement with immediate effect without any liability +(except for the obligations of payment arising prior to the Force +Majeure Event) towards the other Party. + +14. Indemnification +^^^^^^^^^^^^^^^^^^^ + +Under this Agreement, the Licensee agrees to indemnify, defend, and hold +harmless the Licensor, its officers, directors, employees, agents, +affiliates, successors, and assigns from any liabilities, damages, +judgments, awards, losses, costs, expenses, including reasonable +attorney and expert witness fees, arising out of or in connection with +any third-party claims, suits, actions, or proceedings. This indemnity +covers claims resulting from the Licensee’s breach of this Agreement, +negligence, willful misconduct, use of the Licensed Technology, or +related to the Licensee’s products. Excluded from this indemnification +are claims alleging that the Licensee’s authorized use of unmodified +Licensed Technology provided by the Licensor infringes any patent, +trademark, or copyright. The Licensor reserves the right, at its option +and sole discretion, to assume full control of the defense of such +claims with legal counsel of its choice. The Licensee is not permitted +to enter into any third-party agreement that would affect the Licensor’s +rights, constitute an admission of fault by the Licensor, or bind the +Licensor in any manner, without the Licensor’s prior written consent. +The Licensee must promptly notify the Licensor in writing of any claims +brought against the Licensor for which indemnification or defense is +sought, thereby enabling the Licensor to manage legal strategies and +potential settlements effectively. For the avoidance of doubt, the +Licensor does not have any obligation to defend the Licensee or any +End-User against such claim and therefore such defense shall be up to +the Licensor’s sole discretion. If the Licensor decides to not defend a +claim for infringement of Intellectual Property Rights, the Licensee may +defend itself through counsel of its own choice and at its own expense. +The Licensor shall have no liability for any costs, expenses, losses or +damages incurred by the Licensee or any of its End-Users in the event of +such claim. + +15. Governing Law and Jurisdiction +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The Licensor and the Licensee hereby mutually concur that the governance +and interpretation of this Agreement shall be in strict accordance with +the legal statutes and principles as stipulated in Appendix A. + +16. Dispute Resolution +^^^^^^^^^^^^^^^^^^^^^^ + +16.1 It is explicitly agreed that any dispute, controversy, or claim +that may arise out of, or in connection with, this contract, inclusive +of, but not limited to, its formation, interpretation, breach, or +termination, and the arbitrability of the claims posited therein, shall +be definitively resolved through arbitration. This arbitration shall be +conducted in full alignment with the JAMS International Arbitration +Rules. + +16.2 Furthermore, the arbitral tribunal shall be comprised of a sole +arbitrator. The venue for such arbitration proceedings shall be as +delineated in Appendix A of this Agreement. Moreover, the proceedings of +the arbitration shall be conducted in the language specified within +Appendix A. It is hereby declared that the award rendered by the said +arbitrator shall be binding and enforceable, and judgment thereon may be +entered in any court of competent jurisdiction. + +17. Confidentiality +^^^^^^^^^^^^^^^^^^^ + +17.1 During the term of this Agreement and thereafter until Confidential +Information becomes subject to one or more of the exceptions set forth +in Section 17.3, each Party shall maintain strict confidentiality +regarding the other Party’s Confidential Information. “Confidential +Information” shall include, but is not limited to, the Licensed +Technology, trade secrets, business plans, strategies, customer +information, and any other proprietary or sensitive information +disclosed by one Party to the other. + +17.2 Each Party agrees to use the Confidential Information solely for +the purposes of exercising its rights and fulfilling its obligations +under this Agreement. The receiving Party shall not disclose or permit +access to Confidential Information to any third party, except to its +Employees, Contractors, or agents who need to know such information for +the purposes of this Agreement and who are bound by confidentiality +obligations no less protective than those set forth herein. + +17.3 The confidentiality obligations shall not apply to information +that: (a) is or becomes publicly available through no fault of the +receiving Party; (b) was known to the receiving Party prior to +disclosure; (c) is independently developed by the receiving Party +without use of or reference to the Confidential Information; or (d) is +required to be disclosed by law or court order, provided that the +receiving Party gives prompt notice to the disclosing Party to enable it +to seek a protective order. + +-------------- + +GNU General Public License version 3 +------------------------------------ + +Preamble +^^^^^^^^ + +The GNU General Public License is a free, copyleft license for software +and other kinds of works. + +The licenses for most software and other practical works are designed to +take away your freedom to share and change the works. By contrast, the +GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program–to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not price. +Our General Public Licenses are designed to make sure that you have the +freedom to distribute copies of free software (and charge for them if +you wish), that you receive source code or can get it if you want it, +that you can change the software or use pieces of it in new free +programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these +rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis +or for a fee, you must pass on to the recipients the same freedoms that +you received. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) +assert copyright on the software, and (2) offer you this License giving +you legal permission to copy, distribute and/or modify it. + +For the developers’ and authors’ protection, the GPL clearly explains +that there is no warranty for this free software. For both users’ and +authors’ sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of protecting +users’ freedom to change the software. The systematic pattern of such +abuse occurs in the area of products for individuals to use, which is +precisely where it is most unacceptable. Therefore, we have designed +this version of the GPL to prohibit the practice for those products. If +such problems arise substantially in other domains, we stand ready to +extend this provision to those domains in future versions of the GPL, as +needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + +**TERMS AND CONDITIONS** + +0. Definitions. +^^^^^^^^^^^^^^^ + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this +License. Each licensee is addressed as “you”. “Licensees” and +“recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a “modified version” of the +earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on +the Program. + +To “propagate” a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to +the extent that it includes a convenient and prominently visible feature +that (1) displays an appropriate copyright notice, and (2) tells the +user that there is no warranty for the work (except to the extent that +warranties are provided), that licensees may convey the work under this +License, and how to view a copy of this License. If the interface +presents a list of user commands or options, such as a menu, a prominent +item in the list meets this criterion. + +1. Source Code. +^^^^^^^^^^^^^^^ + +The “source code” for a work means the preferred form of the work for +making modifications to it. “Object code” means any non-source form of a +work. + +A “Standard Interface” means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that is +widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that Major +Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A “Major +Component”, in this context, means a major essential component (kernel, +window system, and so on) of the specific operating system (if any) on +which the executable work runs, or a compiler used to produce the work, +or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the +source code needed to generate, install, and (for an executable work) +run the object code and to modify the work, including scripts to control +those activities. However, it does not include the work’s System +Libraries, or general-purpose tools or generally available free programs +which are used unmodified in performing those activities but which are +not part of the work. For example, Corresponding Source includes +interface definition files associated with source files for the work, +and the source code for shared libraries and dynamically linked +subprograms that the work is specifically designed to require, such as +by intimate data communication or control flow between those subprograms +and other parts of the work. + +The Corresponding Source need not include anything that users can +regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same +work. + +2. Basic Permissions. +^^^^^^^^^^^^^^^^^^^^^ + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, +without conditions so long as your license otherwise remains in force. +You may convey covered works to others for the sole purpose of having +them make modifications exclusively for you, or provide you with +facilities for running those works, provided that you comply with the +terms of this License in conveying all material for which you do not +control copyright. Those thus making or running the covered works for +you must do so exclusively on your behalf, under your direction and +control, on terms that prohibit them from making any copies of your +copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes +it unnecessary. + +3. Protecting Users’ Legal Rights From Anti-Circumvention Law. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article 11 +of the WIPO copyright treaty adopted on 20 December 1996, or similar +laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to the +covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work’s +users, your or third parties’ legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You may convey verbatim copies of the Program’s source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; keep +intact all notices stating that this License and any non-permissive +terms added in accord with section 7 apply to the code; keep intact all +notices of the absence of any warranty; and give all recipients a copy +of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and +you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the terms +of section 4, provided that you also meet all of these conditions: + +- + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + +- + + b) The work must carry prominent notices stating that it is released + under this License and any conditions added under section 7. This + requirement modifies the requirement in section 4 to “keep intact + all notices”. + +- + + c) You must license the entire work, as a whole, under this License + to anyone who comes into possession of a copy. This License will + therefore apply, along with any applicable section 7 additional + terms, to the whole of the work, and all its parts, regardless of + how they are packaged. This License gives no permission to license + the work in any other way, but it does not invalidate such + permission if you have separately received it. + +- + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, and +which are not combined with it such as to form a larger program, in or +on a volume of a storage or distribution medium, is called an +“aggregate” if the compilation and its resulting copyright are not used +to limit the access or legal rights of the compilation’s users beyond +what the individual works permit. Inclusion of a covered work in an +aggregate does not cause this License to apply to the other parts of the +aggregate. + +6. Conveying Non-Source Forms. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You may convey a covered work in object code form under the terms of +sections 4 and 5, provided that you also convey the machine-readable +Corresponding Source under the terms of this License, in one of these +ways: + +- + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + +- + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the Corresponding + Source from a network server at no charge. + +- + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + +- + + d) Convey the object code by offering access from a designated place + (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + +- + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be included +in conveying the object code work. + +A “User Product” is either (1) a “consumer product”, which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for +incorporation into a dwelling. In determining whether a product is a +consumer product, doubtful cases shall be resolved in favor of coverage. +For a particular product received by a particular user, “normally used” +refers to a typical or common use of that class of product, regardless +of the status of the particular user or of the way in which the +particular user actually uses, or expects or is expected to use, the +product. A product is a consumer product regardless of whether the +product has substantial commercial, industrial or non-consumer uses, +unless such uses represent the only significant mode of use of the +product. + +“Installation Information” for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product +from a modified version of its Corresponding Source. The information +must suffice to ensure that the continued functioning of the modified +object code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied by +the Installation Information. But this requirement does not apply if +neither you nor any third party retains the ability to install modified +object code on the User Product (for example, the work has been +installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in +accord with this section must be in a format that is publicly documented +(and with an implementation available to the public in source code +form), and must require no special password or key for unpacking, +reading or copying. + +7. Additional Terms. +^^^^^^^^^^^^^^^^^^^^ + +“Additional permissions” are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by this +License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove +any additional permissions from that copy, or from any part of it. +(Additional permissions may be written to require their own removal in +certain cases when you modify the work.) You may place additional +permissions on material, added by you to a covered work, for which you +have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders +of that material) supplement the terms of this License with terms: + +- + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + +- + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + +- + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + +- + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + +- + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + +- + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions + of it) with contractual assumptions of liability to the recipient, + for any liability that these contractual assumptions directly + impose on those licensors and authors. + +All other non-permissive additional terms are considered “further +restrictions” within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains a +further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms of +that license document, provided that the further restriction does not +survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must +place, in the relevant source files, a statement of the additional terms +that apply to those files, or a notice indicating where to find the +applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; the above +requirements apply either way. + +8. Termination. +^^^^^^^^^^^^^^^ + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally terminates +your license, and (b) permanently, if the copyright holder fails to +notify you of the violation by some reasonable means prior to 60 days +after the cessation. + +Moreover, your license from a particular copyright holder is reinstated +permanently if the copyright holder notifies you of the violation by +some reasonable means, this is the first time you have received notice +of violation of this License (for any work) from that copyright holder, +and you cure the violation prior to 30 days after your receipt of the +notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You are not required to accept this License in order to receive or run a +copy of the Program. Ancillary propagation of a covered work occurring +solely as a consequence of using peer-to-peer transmission to receive a +copy likewise does not require acceptance. However, nothing other than +this License grants you permission to propagate or modify any covered +work. These actions infringe copyright if you do not accept this +License. Therefore, by modifying or propagating a covered work, you +indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered work +results from an entity transaction, each party to that transaction who +receives a copy of the work also receives whatever licenses to the work +the party’s predecessor in interest had or could give under the previous +paragraph, plus a right to possession of the Corresponding Source of the +work from the predecessor in interest, if the predecessor has it or can +get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may not +impose a license fee, royalty, or other charge for exercise of rights +granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that any +patent claim is infringed by making, using, selling, offering for sale, +or importing the Program or any portion of it. + +11. Patents. +^^^^^^^^^^^^ + +A “contributor” is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The work +thus licensed is called the contributor’s “contributor version”. + +A contributor’s “essential patent claims” are all patent claims owned or +controlled by the contributor, whether already acquired or hereafter +acquired, that would be infringed by some manner, permitted by this +License, of making, using, or selling its contributor version, but do +not include claims that would be infringed only as a consequence of +further modification of the contributor version. For purposes of this +definition, “control” includes the right to grant patent sublicenses in +a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor’s essential patent claims, to make, +use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To “grant” such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and +the Corresponding Source of the work is not available for anyone to +copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. “Knowingly relying” means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient’s use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify or +convey a specific copy of the covered work, then the patent license you +grant is automatically extended to all recipients of the covered work +and works based on it. + +A patent license is “discriminatory” if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on +the non-exercise of one or more of the rights that are specifically +granted under this License. You may not convey a covered work if you are +a party to an arrangement with a third party that is in the business of +distributing software, under which you make payment to the third party +based on the extent of your activity of conveying the work, and under +which the third party grants, to any of the parties who would receive +the covered work from you, a discriminatory patent license (a) in +connection with copies of the covered work conveyed by you (or copies +made from those copies), or (b) primarily for and in connection with +specific products or compilations that contain the covered work, unless +you entered into that arrangement, or that patent license was granted, +prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any +implied license or other defenses to infringement that may otherwise be +available to you under applicable patent law. + +12. No Surrender of Others’ Freedom. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not convey it at all. For example, if you agree to terms that +obligate you to collect a royalty for further conveying from those to +whom you convey the Program, the only way you could satisfy both those +terms and this License would be to refrain entirely from conveying the +Program. + +13. Use with the GNU Affero General Public License. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Notwithstanding any other provision of this License, you have permission +to link or combine any covered work with a work licensed under version 3 +of the GNU Affero General Public License into a single combined work, +and to convey the resulting work. The terms of this License will +continue to apply to the part which is the covered work, but the special +requirements of the GNU Affero General Public License, section 13, +concerning interaction through a network will apply to the combination +as such. + +14. Revised Versions of this License. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies that a certain numbered version of the GNU General Public +License “or any later version” applies to it, you have the option of +following the terms and conditions either of that numbered version or of +any later version published by the Free Software Foundation. If the +Program does not specify a version number of the GNU General Public +License, you may choose any version ever published by the Free Software +Foundation. + +If the Program specifies that a proxy can decide which future versions +of the GNU General Public License can be used, that proxy’s public +statement of acceptance of a version permanently authorizes you to +choose that version for the Program. + +Later license versions may give you additional or different permissions. +However, no additional obligations are imposed on any author or +copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT +WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF +THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR +CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT +NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES +SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE +WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN +ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If the disclaimer of warranty and limitation of liability provided above +cannot be given local legal effect according to their terms, reviewing +courts shall apply local law that most closely approximates an absolute +waiver of all civil liability in connection with the Program, unless a +warranty or assumption of liability accompanies a copy of the Program in +return for a fee. + +.. Links + +.. _website: https://simpleble.org \ No newline at end of file diff --git a/third_party/SimpleBLE/docs/overview.rst b/third_party/SimpleBLE/docs/overview.rst index e779c1ba4..684a1fe97 100644 --- a/third_party/SimpleBLE/docs/overview.rst +++ b/third_party/SimpleBLE/docs/overview.rst @@ -2,11 +2,12 @@ Overview -------- The SimpleBLE project aims to provide fully cross-platform BLE libraries and bindings -for Python and C++, designed for simplicity and ease of use with a licencing scheme -chosen to be friendly towards commercial use. All specific operating system quirks -are handled internally to provide a consistent behavior across all platforms. The -libraries also provide first-class support for vendorization of all third-party -dependencies, allowing for easy integration into existing projects. +for C++, Python, Rust and other languages, designed for simplicity and ease of use. +All specific operating system quirks are handled internally to provide a consistent behavior +and API across all platforms. The libraries also provide first-class support for vendorization +of all third-party dependencies, allowing for easy integration into existing projects. + +**NOTICE: Since February 20, 2024 the license terms of SimpleBLE have changed. Please make sure to read and understand the new licensing scheme.** This repository offers the source code for the following related libraries: @@ -19,50 +20,61 @@ If you want to use SimpleBLE and need help. **Please do not hesitate to reach ou * Visit our `ReadTheDocs`_ page. * Join our `Discord`_ server. -* Contact me: ``kevin at dewald dot me`` (Dedicated consulting services available) - -Are you using SimpleBLE on your own project and would like to see it featured here? -Reach out and I'll add a link to it below! +* Contact us: ``contact at simpleble dot org`` +* Visit our `website`_ for more information. Supported platforms -------------------- -=========== ============= =================================== ===== -Windows Linux MacOS iOS -=========== ============= =================================== ===== -Windows 10+ Ubuntu 20.04+ 10.15+ (except 12.0, 12.1 and 12.2) 15.0+ -=========== ============= =================================== ===== +^^^^^^^^^^^^^^^^^^^ -Projects using SimpleBLE ------------------------- -Don't forget to check out the following projects using SimpleBLE: +Windows +""""""" +* **Supported Versions:** Windows 10 and newer +* **Notes:** -* `GDSimpleBLE`_ -* `BrainFlow`_ + - WSL does not support Bluetooth. + - Only a single adapter is supported by the OS backend. -Contributing ------------- -Pull requests are welcome. For major changes, please open an issue first to discuss -what you would like to change. +Linux +""""" +* **Supported Distributions:** Ubuntu 20.04 and newer +* **Notes:** -License -------- + - While Ubuntu is our primary supported distribution, the software may work on other major distributions using Bluez as their Bluetooth backend. -All components within this project that have not been bundled from -external creators, are licensed under the terms of the `MIT Licence`_. +MacOS +""""" +* **Supported Versions:** 10.15 (Catalina) and newer +* **Exceptions:** MacOS 12.0, 12.1, and 12.2 have a bug where the adapter won't return any peripherals after scanning. +* **Notes:** -.. Links + - Only a single adapter is supported by the OS backend. -.. _SimplePyBLE: https://pypi.org/project/simplepyble/ +iOS +""" +* **Supported Versions:** iOS 15.0 and newer +* **Notes:** -.. _MIT Licence: https://github.com/OpenBluetoothToolbox/SimpleBLE/blob/main/LICENCE.md + - Older versions of iOS might work but haven't been formally tested. -.. _fmtlib: https://github.com/fmtlib/fmt +Android +""""""" +* **Supported Versions:** API 31 and newer +* **Notes:** -.. _Discord: https://discord.gg/N9HqNEcvP3 + - Capabilities are still in an alpha stage, but should be good enough for initial testing. + - Older APIs are missing certain features of the JVM API that are required by SimpleBLE +.. Links + +.. _website: https://simpleble.org +.. _SimplePyBLE: https://pypi.org/project/simplepyble/ +.. _SimpleRsBLE: https://crates.io/crates/simplersble +.. _Discord: https://discord.gg/N9HqNEcvP3 .. _ReadTheDocs: https://simpleble.readthedocs.io/en/latest/ .. Other projects using SimpleBLE .. _GDSimpleBLE: https://github.com/jferdelyi/GDSimpleBLE -.. _BrainFlow: https://github.com/brainflow-dev/brainflow \ No newline at end of file +.. _BrainFlow: https://github.com/brainflow-dev/brainflow +.. _InsideBlue: https://github.com/eriklins/InsideBlue-BLE-Tool +.. _NodeWebBluetooth: https://github.com/thegecko/webbluetooth diff --git a/third_party/SimpleBLE/docs/requirements.txt b/third_party/SimpleBLE/docs/requirements.txt index eb4d45367..df91dccaa 100644 --- a/third_party/SimpleBLE/docs/requirements.txt +++ b/third_party/SimpleBLE/docs/requirements.txt @@ -1,3 +1,4 @@ sphinx==5.1.1 sphinx_rtd_theme==1.0.0 breathe==4.34.0 +sphinxcontrib-mermaid==0.9.2 \ No newline at end of file diff --git a/third_party/SimpleBLE/docs/simpleble/api.rst b/third_party/SimpleBLE/docs/simpleble/api.rst index f4f06d65c..1bff51a46 100644 --- a/third_party/SimpleBLE/docs/simpleble/api.rst +++ b/third_party/SimpleBLE/docs/simpleble/api.rst @@ -34,6 +34,9 @@ Standard API :members: :undoc-members: +.. doxygentypedef:: SimpleBLE::ByteArray + :project: simpleble + Safe API ======== @@ -55,3 +58,11 @@ C API .. doxygenfile:: peripheral.h :project: simpleble + +External API +============ + +.. doxygenclass:: kvn::bytearray + :project: external + :members: + :undoc-members: diff --git a/third_party/SimpleBLE/docs/simpleble/faq.rst b/third_party/SimpleBLE/docs/simpleble/faq.rst index 944a7de10..5f669f74d 100644 --- a/third_party/SimpleBLE/docs/simpleble/faq.rst +++ b/third_party/SimpleBLE/docs/simpleble/faq.rst @@ -13,6 +13,13 @@ UUID is not persistent across reboots, so you should not use it to identify a peripheral. Instead, you should use the name of the peripheral, which is persistent across reboots. +**I get a "Bluetooth not enabled" warning on Windows, despite Bluetooth being enabled.** + +This is a known issue when running a version of SimpleBLE built for a 32-bit architecture +on a 64-bit Windows machine. The issue is that the underlying Windows API will not allow +us to query the state of the Bluetooth adapter when running in 32-bit mode. The solution +is to consume a 64-bit version of SimpleBLE instead. + **What is the purpose behind the plain interface?** Building SimpleBLE with the plain-flavored interface allows you to use a version of the diff --git a/third_party/SimpleBLE/docs/simpleble/tutorial.rst b/third_party/SimpleBLE/docs/simpleble/tutorial.rst index 40638943d..0ae5026f1 100644 --- a/third_party/SimpleBLE/docs/simpleble/tutorial.rst +++ b/third_party/SimpleBLE/docs/simpleble/tutorial.rst @@ -233,18 +233,16 @@ Layers and their responsibilities .. _scan (cpp): https://github.com/OpenBluetoothToolbox/SimpleBLE/blob/main/examples/simpleble/cpp/scan/scan.cpp -.. _scan (c): https://github.com/OpenBluetoothToolbox/SimpleBLE/blob/main/examples/simpleble/c/scan/scan.c - .. _connect (cpp): https://github.com/OpenBluetoothToolbox/SimpleBLE/blob/main/examples/simpleble/cpp/connect/connect.cpp .. _connect_safe (cpp): https://github.com/OpenBluetoothToolbox/SimpleBLE/blob/main/examples/simpleble/cpp/connect_safe/connect_safe.cpp -.. _connect (c): https://github.com/OpenBluetoothToolbox/SimpleBLE/blob/main/examples/simpleble/c/connect/connect.c - .. _read: https://github.com/OpenBluetoothToolbox/SimpleBLE/blob/main/examples/simpleble/cpp/read/read.cpp .. _write: https://github.com/OpenBluetoothToolbox/SimpleBLE/blob/main/examples/simpleble/cpp/write/write.cpp .. _notify (cpp): https://github.com/OpenBluetoothToolbox/SimpleBLE/blob/main/examples/simpleble/cpp/notify/notify.cpp -.. _notify (c): https://github.com/OpenBluetoothToolbox/SimpleBLE/blob/main/examples/simpleble/c/notify/notify.c +.. _connect (c): https://github.com/OpenBluetoothToolbox/SimpleBLE/blob/main/examples/simpleble/c/connect.c +.. _scan (c): https://github.com/OpenBluetoothToolbox/SimpleBLE/blob/main/examples/simpleble/c/scan.c +.. _notify (c): https://github.com/OpenBluetoothToolbox/SimpleBLE/blob/main/examples/simpleble/c/notify.c diff --git a/third_party/SimpleBLE/docs/simpleble/usage.rst b/third_party/SimpleBLE/docs/simpleble/usage.rst index 6cd520819..3bcf04b82 100644 --- a/third_party/SimpleBLE/docs/simpleble/usage.rst +++ b/third_party/SimpleBLE/docs/simpleble/usage.rst @@ -6,32 +6,105 @@ SimpleBLE works on Windows, Linux, MacOS and iOS. Please follow the instructions to build and run SimpleBLE in your specific environment. -Building and Installing the Library (Source) -============================================ +System Requirements +=================== When building SimpleBLE from source, you will need some dependencies based on your current operating system. -**Linux** :: +**NOTE:** WSL does not support Bluetooth. + +General Requirements +-------------------- + + - `CMake`_ (Version 3.21 or higher) + +Linux +----- + +APT-based Distros +~~~~~~~~~~~~~~~~~ + + - `libdbus-1-dev` (install via ``sudo apt install libdbus-1-dev``) + +RPM-based Distros +~~~~~~~~~~~~~~~~~ + + - `dbus-devel` + - On Fedora, install via ``sudo dnf install dbus-devel`` + - On CentOS, install via ``sudo yum install dbus-devel`` + +Windows +------- + + - `Windows SDK` (Version 10.0.19041.0 or higher) + +MacOS +----- + + - `Xcode Command Line Tools` (install via ``xcode-select --install``) + +Android +------- + + - `Android Studio` + - `Android NDK` (Version 25 or higher. Older versions might work but haven't been thoroughly tested.) + + +Building and Installing SimpleBLE (Source) +============================================ + +Compiling the library is done using `CMake`_ and relies heavily on plenty of CMake +functionality. It is strongly suggested that you get familiarized with CMake before +blindly following the instructions below. + + +Building SimpleBLE +------------------ + +You can use the following commands to build SimpleBLE: :: - sudo apt install libdbus-1-dev + cmake -S -B build_simpleble + cmake --build build_simpleble -j7 -**Windows** :: +Note that if you want to modify the build configuration, you can do so by passing +additional arguments to the ``cmake`` command. For example, to build a shared library +set the ``BUILD_SHARED_LIBS`` CMake variable to ``TRUE`` :: - `Windows SDK `_ (Version 10.0.19041.0 or higher) + cmake -S -B build_simpleble -DBUILD_SHARED_LIBS=TRUE -The included CMake build script can be used to build SimpleBLE. -CMake is freely available for download from https://www.cmake.org/download/. :: +To build a plain-flavored version of the library, set the ``SIMPLEBLE_PLAIN`` CMake +variable to ``TRUE`` :: - cd - mkdir build && cd build - cmake .. -DSIMPLEBLE_LOG_LEVEL=[VERBOSE|DEBUG|INFO|WARNING|ERROR|FATAL] - cmake --build . -j7 - sudo cmake --install . + cmake -S -B build_simpleble -DSIMPLEBLE_PLAIN=TRUE -To build a shared library set the ``BUILD_SHARED_LIBS`` CMake variable to ``TRUE`` :: +To modify the log level, set the ``SIMPLEBLE_LOG_LEVEL`` CMake variable to one of the +following values: ``VERBOSE``, ``DEBUG``, ``INFO``, ``WARN``, ``ERROR``, ``FATAL`` :: - cmake -DBUILD_SHARED_LIBS=TRUE ... + cmake -S -B build_simpleble -DSIMPLEBLE_LOG_LEVEL=DEBUG + +**(Linux only)** To force the usage of the DBus session bus, enable the ``SIMPLEBLE_USE_SESSION_DBUS`` flag :: + + cmake -S -B build_simplebluez -DSIMPLEBLE_USE_SESSION_DBUS=TRUE + +Installing SimpleBLE +-------------------- + +To install SimpleBLE, you can use the following commands: :: + + cmake --install build_simpleble + +Note that if you want to modify the installation configuration, you can do so by passing +additional arguments to the ``cmake`` command. For example, to install the library to +a specific location, set the ``CMAKE_INSTALL_PREFIX`` CMake variable to the desired +location :: + + cmake --install build_simpleble --prefix /usr/local + +Note that on Linux and MacOS, you will need to run the ``cmake --install`` command +with ``sudo`` privileges. :: + + sudo cmake --install build_simpleble Usage with CMake (Installed) @@ -42,6 +115,9 @@ Once SimpleBLE has been installed, it can be consumed from within CMake:: find_package(simpleble REQUIRED CONFIG) target_link_libraries( simpleble::simpleble) +Note that this example assumes that SimpleBLE has been installed to a location +that is part of the default CMake module path. + Usage with CMake (Local) ============================= @@ -102,32 +178,47 @@ the following CMake options available: - ``LIBFMT_GIT_REPOSITORY``: The git repository to use for fmtlib. - - ``LIBFMT_GIT_TAG``: The git tag to use for fmtlib. *(Default: v8.1.1)* + - ``LIBFMT_GIT_TAG``: The git tag to use for fmtlib. *(Default: v9.1.0)* - ``LIBFMT_LOCAL_PATH``: The local path to use for fmtlib. *(Default: None)* -Build Examples -============== +Usage alongside native code in Android +====================================== -Use the following instructions to build the provided SimpleBLE examples: :: +When using SimpleBLE alongside native code in Android, you must include a small +Android dependency module that includes some necessary bridge classes used by SimpleBLE. +This is required because the Android JVM doesn't allow programatic definition of +derived classes, which forces us to bring these definitions in externally. + +To include this dependency module, add the following to your `settings.gradle` file: + +```groovy +includeBuild("path/to/simpleble/src/backends/android/simpleble-bridge") { + dependencySubstitution { + substitute module("org.simpleble.android.bridge:simpleble-bridge") with project(":") + } +} +``` + +```kotlin +includeBuild("path/to/simpleble/src/backends/android/simpleble-bridge") { + dependencySubstitution { + substitute(module("org.simpleble.android.bridge:simpleble-bridge")).using(project(":")) + } +} +``` - cd - mkdir build && cd build - cmake -DSIMPLEBLE_LOCAL=ON ../examples/simpleble - cmake --build . -j7 +**NOTE:** We will provide Maven packages in the future. -Plain-flavored Build -==================== +Build Examples +============== -Use the following instructions to build SimpleBLE with the plain-flavored API: :: +Use the following instructions to build the provided SimpleBLE examples: :: - cd - mkdir build && cd build - cmake .. -DSIMPLEBLE_PLAIN=ON - cmake --build . -j7 - sudo cmake --install . + cmake -S /examples/simpleble -B build_simpleble_examples -DSIMPLEBLE_LOCAL=ON + cmake --build build_simpleble_examples -j7 Testing @@ -145,11 +236,9 @@ Unit Tests To run the unit tests, run the following command: :: - cd - mkdir build && cd build - cmake .. -DCMAKE_BUILD_TYPE=Debug -DSIMPLEBLE_TEST=ON - cmake --build . -j7 - ./bin/simpleble_test + cmake -S -B build_simpleble_test -DSIMPLEBLE_TEST=ON + cmake --build build_simpleble_test -j7 + ./build_simpleble_test/bin/simpleble_test Address Sanitizer Tests @@ -157,11 +246,9 @@ Address Sanitizer Tests To run the address sanitizer tests, run the following command: :: - cd - mkdir build && cd build - cmake .. -DCMAKE_BUILD_TYPE=Debug -DSIMPLEBLE_SANITIZE=Address -DSIMPLEBLE_TEST=ON - cmake --build . -j7 - PYTHONMALLOC=malloc ./bin/simpleble_test + cmake -S -B build_simpleble_test -DSIMPLEBLE_SANITIZE=Address -DSIMPLEBLE_TEST=ON + cmake --build build_simpleble_test -j7 + PYTHONMALLOC=malloc ./build_simpleble_test/bin/simpleble_test It's important for ``PYTHONMALLOC`` to be set to ``malloc``, otherwise the tests will fail due to Python's memory allocator from triggering false positives. @@ -172,15 +259,17 @@ Thread Sanitizer Tests To run the thread sanitizer tests, run the following command: :: - cd - mkdir build && cd build - cmake .. -DCMAKE_BUILD_TYPE=Debug -DSIMPLEBLE_SANITIZE=Thread -DSIMPLEBLE_TEST=ON - cmake --build . -j7 - ./bin/simpleble_test + cmake -S -B build_simpleble_test -DSIMPLEBLE_SANITIZE=Thread -DSIMPLEBLE_TEST=ON + cmake --build build_simpleble_test -j7 + ./build_simpleble_test/bin/simpleble_test .. Links +.. _CMake: https://cmake.org/ + +.. _Windows SDK: https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk + .. _cmake-init-fetchcontent: https://github.com/friendlyanon/cmake-init-fetchcontent .. _fmtlib: https://github.com/fmtlib/fmt diff --git a/third_party/SimpleBLE/docs/simplebluez/usage.rst b/third_party/SimpleBLE/docs/simplebluez/usage.rst index 98a23d562..25e5aac5b 100644 --- a/third_party/SimpleBLE/docs/simplebluez/usage.rst +++ b/third_party/SimpleBLE/docs/simplebluez/usage.rst @@ -2,27 +2,85 @@ Usage ===== -SimpleBluez should work on any Linux environment supporting DBus. To install -the necessary dependencies on Debian-based systems, use the following command: :: +SimpleBluez should work on any Linux environment supporting DBus and Bluez. +Please follow the instructions below to build and run SimpleBluez in your specific environment. - sudo apt install libdbus-1-dev +System Requirements +=================== + +When building SimpleBluez from source, you will need some dependencies based on your +current operating system. + +General Requirements +-------------------- + + - `CMake`_ (Version 3.21 or higher) + +Linux +----- + +APT-based Distros +~~~~~~~~~~~~~~~~~ + + - `libdbus-1-dev` (install via ``sudo apt install libdbus-1-dev``) + +RPM-based Distros +~~~~~~~~~~~~~~~~~ + + - `dbus-devel` + - On Fedora, install via ``sudo dnf install dbus-devel`` + - On CentOS, install via ``sudo yum install dbus-devel`` Building and Installing the Library (Source) ============================================ -The included CMake build script can be used to build SimpleBluez. -CMake is freely available for download from https://www.cmake.org/download/. :: +Compiling the library is done using `CMake`_ and relies heavily on plenty of CMake +functionality. It is strongly suggested that you get familiarized with CMake before +blindly following the instructions below. + + +Building SimpleBluez +-------------------- + +You can use the following commands to build SimpleBluez: :: + + cmake -S -B build_simplebluez + cmake --build build_simplebluez -j7 - cd - mkdir build && cd build - cmake .. -DSIMPLEBLUEZ_LOG_LEVEL=[VERBOSE|DEBUG|INFO|WARNING|ERROR|FATAL] - cmake --build . -j7 - sudo cmake --install . +Note that if you want to modify the build configuration, you can do so by passing +additional arguments to the ``cmake`` command. For example, to build a shared library +set the ``BUILD_SHARED_LIBS`` CMake variable to ``TRUE`` :: -To build a shared library set the ``BUILD_SHARED_LIBS`` CMake variable to ``TRUE`` :: + cmake -S -B build_simplebluez -DBUILD_SHARED_LIBS=TRUE - cmake -DBUILD_SHARED_LIBS=TRUE ... +To modify the log level, set the ``SIMPLEBLUEZ_LOG_LEVEL`` CMake variable to one of the +following values: ``VERBOSE``, ``DEBUG``, ``INFO``, ``WARN``, ``ERROR``, ``FATAL`` :: + + cmake -S -B build_simplebluez -DSIMPLEBLUEZ_LOG_LEVEL=DEBUG + +To force the usage of the DBus session bus, enable the ``SIMPLEBLUEZ_USE_SESSION_DBUS`` flag :: + + cmake -S -B build_simplebluez -DSIMPLEBLUEZ_USE_SESSION_DBUS=TRUE + +Installing SimpleBluez +---------------------- + +To install SimpleBluez, you can use the following commands: :: + + cmake --install build_simplebluez + +Note that if you want to modify the installation configuration, you can do so by passing +additional arguments to the ``cmake`` command. For example, to install the library to +a specific location, set the ``CMAKE_INSTALL_PREFIX`` CMake variable to the desired +location :: + + cmake --install build_simplebluez --prefix /usr/local + +Note that on Linux and MacOS, you will need to run the ``cmake --install`` command +with ``sudo`` privileges. :: + + sudo cmake --install build_simplebluez Usage with CMake (Installed) @@ -103,10 +161,8 @@ Build Examples Use the following instructions to build the provided SimpleBluez examples: :: - cd - mkdir build && cd build - cmake -DSIMPLEBLUEZ_LOCAL=ON ../examples/simplebluez - cmake --build . -j7 + cmake -S /examples/simplebluez -B build_simplebluez_examples -DSIMPLEBLUEZ_LOCAL=ON + cmake --build build_simplebluez_examples -j7 Testing @@ -124,11 +180,9 @@ Unit Tests To run the unit tests, run the following command: :: - cd - mkdir build && cd build - cmake .. -DCMAKE_BUILD_TYPE=Debug -DSIMPLEBLUEZ_TEST=ON - cmake --build . -j7 - ./bin/simplebluez_test + cmake -S -B build_simplebluez_test -DSIMPLEBLUEZ_TEST=ON + cmake --build build_simplebluez_test -j7 + ./build_simplebluez_test/bin/simplebluez_test Address Sanitizer Tests @@ -136,11 +190,9 @@ Address Sanitizer Tests To run the address sanitizer tests, run the following command: :: - cd - mkdir build && cd build - cmake .. -DCMAKE_BUILD_TYPE=Debug -DSIMPLEBLUEZ_SANITIZE=Address -DSIMPLEBLUEZ_TEST=ON - cmake --build . -j7 - PYTHONMALLOC=malloc ./bin/simplebluez_test + cmake -S -B build_simplebluez_test -DSIMPLEBLUEZ_SANITIZE=Address -DSIMPLEBLUEZ_TEST=ON + cmake --build build_simplebluez_test -j7 + PYTHONMALLOC=malloc ./build_simplebluez_test/bin/simplebluez_test It's important for ``PYTHONMALLOC`` to be set to ``malloc``, otherwise the tests will fail due to Python's memory allocator from triggering false positives. @@ -151,15 +203,15 @@ Thread Sanitizer Tests To run the thread sanitizer tests, run the following command: :: - cd - mkdir build && cd build - cmake .. -DCMAKE_BUILD_TYPE=Debug -DSIMPLEBLUEZ_SANITIZE=Thread -DSIMPLEBLUEZ_TEST=ON - cmake --build . -j7 - ./bin/simplebluez_test + cmake -S -B build_simplebluez_test -DSIMPLEBLUEZ_SANITIZE=Thread -DSIMPLEBLUEZ_TEST=ON + cmake --build build_simplebluez_test -j7 + ./build_simplebluez_test/bin/simplebluez_test .. Links +.. _CMake: https://cmake.org/ + .. _cmake-init-fetchcontent: https://github.com/friendlyanon/cmake-init-fetchcontent .. _fmtlib: https://github.com/fmtlib/fmt diff --git a/third_party/SimpleBLE/docs/simpledbus/usage.rst b/third_party/SimpleBLE/docs/simpledbus/usage.rst index 5e7e04fcf..41c35a3a3 100644 --- a/third_party/SimpleBLE/docs/simpledbus/usage.rst +++ b/third_party/SimpleBLE/docs/simpledbus/usage.rst @@ -2,27 +2,85 @@ Usage ===== -SimpleDBus should work on any Linux environment supporting DBus. To install -the necessary dependencies on Debian-based systems, use the following command: :: +SimpleDBus should work on any Linux environment supporting DBus. +Please follow the instructions below to build and run SimpleDBus in your specific environment. - sudo apt install libdbus-1-dev +System Requirements +=================== + +When building SimpleDBus from source, you will need some dependencies based on your +current operating system. + +General Requirements +-------------------- + + - `CMake`_ (Version 3.21 or higher) + +Linux +----- + +APT-based Distros +~~~~~~~~~~~~~~~~~ + + - `libdbus-1-dev` (install via ``sudo apt install libdbus-1-dev``) + +RPM-based Distros +~~~~~~~~~~~~~~~~~ + + - `dbus-devel` + - On Fedora, install via ``sudo dnf install dbus-devel`` + - On CentOS, install via ``sudo yum install dbus-devel`` Building and Installing the Library (Source) ============================================ -The included CMake build script can be used to build SimpleDBus. -CMake is freely available for download from https://www.cmake.org/download/. :: +Compiling the library is done using `CMake`_ and relies heavily on plenty of CMake +functionality. It is strongly suggested that you get familiarized with CMake before +blindly following the instructions below. + + +Building SimpleDBus +------------------- + +You can use the following commands to build SimpleDBus: :: + + cmake -S -B build_simpledbus + cmake --build build_simpledbus -j7 - cd - mkdir build && cd build - cmake .. -DSIMPLEDBUS_LOG_LEVEL=[VERBOSE|DEBUG|INFO|WARNING|ERROR|FATAL] - cmake --build . -j7 - sudo cmake --install . +Note that if you want to modify the build configuration, you can do so by passing +additional arguments to the ``cmake`` command. For example, to build a shared library +set the ``BUILD_SHARED_LIBS`` CMake variable to ``TRUE`` :: -To build a shared library set the ``BUILD_SHARED_LIBS`` CMake variable to ``TRUE`` :: + cmake -S -B build_simpledbus -DBUILD_SHARED_LIBS=TRUE - cmake -DBUILD_SHARED_LIBS=TRUE ... +To modify the log level, set the ``SIMPLEDBUS_LOG_LEVEL`` CMake variable to one of the +following values: ``VERBOSE``, ``DEBUG``, ``INFO``, ``WARN``, ``ERROR``, ``FATAL`` :: + + cmake -S -B build_simpledbus -DSIMPLEDBUS_LOG_LEVEL=DEBUG + +To force the usage of the DBus session bus, enable the ``SIMPLEDBUS_USE_SESSION_DBUS`` flag :: + + cmake -S -B build_simpledbus -DSIMPLEDBUS_USE_SESSION_DBUS=TRUE + +Installing SimpleDBus +---------------------- + +To install SimpleDBus, you can use the following commands: :: + + cmake --install build_simpledbus + +Note that if you want to modify the installation configuration, you can do so by passing +additional arguments to the ``cmake`` command. For example, to install the library to +a specific location, set the ``CMAKE_INSTALL_PREFIX`` CMake variable to the desired +location :: + + cmake --install build_simpledbus --prefix /usr/local + +Note that on Linux and MacOS, you will need to run the ``cmake --install`` command +with ``sudo`` privileges. :: + + sudo cmake --install build_simpledbus Usage with CMake (Installed) @@ -103,10 +161,8 @@ Build Examples Use the following instructions to build the provided SimpleDBus examples: :: - cd - mkdir build && cd build - cmake -DSIMPLEDBUS_LOCAL=ON ../examples/simpledbus - cmake --build . -j7 + cmake -S /examples/simpledbus -B build_simpledbus_examples -DSIMPLEDBUS_LOCAL=ON + cmake --build build_simpledbus_examples -j7 Testing @@ -124,11 +180,9 @@ Unit Tests To run the unit tests, run the following command: :: - cd - mkdir build && cd build - cmake .. -DCMAKE_BUILD_TYPE=Debug -DSIMPLEDBUS_TEST=ON - cmake --build . -j7 - ./bin/simpledbus_test + cmake -S -B build_simpledbus_test -DSIMPLEDBUS_TEST=ON + cmake --build build_simpledbus_test -j7 + ./build_simpledbus_test/bin/simpledbus_test Address Sanitizer Tests @@ -136,11 +190,9 @@ Address Sanitizer Tests To run the address sanitizer tests, run the following command: :: - cd - mkdir build && cd build - cmake .. -DCMAKE_BUILD_TYPE=Debug -DSIMPLEDBUS_SANITIZE=Address -DSIMPLEDBUS_TEST=ON - cmake --build . -j7 - PYTHONMALLOC=malloc ./bin/simpledbus_test + cmake -S -B build_simpledbus_test -DSIMPLEDBUS_SANITIZE=Address -DSIMPLEDBUS_TEST=ON + cmake --build build_simpledbus_test -j7 + PYTHONMALLOC=malloc ./build_simpledbus_test/bin/simpledbus_test It's important for ``PYTHONMALLOC`` to be set to ``malloc``, otherwise the tests will fail due to Python's memory allocator from triggering false positives. @@ -151,15 +203,15 @@ Thread Sanitizer Tests To run the thread sanitizer tests, run the following command: :: - cd - mkdir build && cd build - cmake .. -DCMAKE_BUILD_TYPE=Debug -DSIMPLEDBUS_SANITIZE=Thread -DSIMPLEDBUS_TEST=ON - cmake --build . -j7 - ./bin/simpledbus_test + cmake -S -B build_simpledbus_test -DSIMPLEDBUS_SANITIZE=Thread -DSIMPLEDBUS_TEST=ON + cmake --build build_simpledbus_test -j7 + ./build_simpledbus_test/bin/simpledbus_test .. Links +.. _CMake: https://cmake.org/ + .. _cmake-init-fetchcontent: https://github.com/friendlyanon/cmake-init-fetchcontent .. _fmtlib: https://github.com/fmtlib/fmt diff --git a/third_party/SimpleBLE/docs/simpledroidble/usage.rst b/third_party/SimpleBLE/docs/simpledroidble/usage.rst new file mode 100644 index 000000000..a2fabd159 --- /dev/null +++ b/third_party/SimpleBLE/docs/simpledroidble/usage.rst @@ -0,0 +1,47 @@ +===== +Usage +===== + +SimpleDroidBLE is an Android-specific package that provides an API similar to +SimpleBLE, using modern Kotlin idiomatic code. + +This code is currently under active development and some features are not yet +implemented or their API might change, but should be enough to help you get +started. + +Consuming Locally +================= + +If you want to use SimpleDroidBLE as part of your project from a local copy, +you can do so by adding the following to your `settings.gradle` or `settings.gradle.kts`file. +Make sure this include is before your `include(":app")` statement. + +```groovy +includeBuild("path/to/simpledroidble") { + dependencySubstitution { + substitute module("org.simpleble.android:simpledroidble") with project(":simpledroidble") + } +} + +```kotlin +includeBuild("path/to/simpledroidble") { + dependencySubstitution { + substitute(module("org.simpleble.android:simpledroidble")).using(project(":simpledroidble")) + } +} +``` + +Then, inside your `build.gradle` or `build.gradle.kts` file, you can add the +following dependency: + +```groovy +dependencies { + implementation "org.simpleble.android:simpledroidble" +} +``` + +```kotlin +dependencies { + implementation("org.simpleble.android:simpledroidble") +} +``` \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/.gitignore b/third_party/SimpleBLE/examples/simpleble-android/.gitignore new file mode 100644 index 000000000..aa724b770 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/.gitignore b/third_party/SimpleBLE/examples/simpleble-android/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/build.gradle.kts b/third_party/SimpleBLE/examples/simpleble-android/app/build.gradle.kts new file mode 100644 index 000000000..bbd7cd103 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.jetbrainsKotlinAndroid) +} + +android { + namespace = "org.simpleble.examples.android" + compileSdk = 34 + + defaultConfig { + applicationId = "org.simpleble.examples.android" + minSdk = 31 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + viewBinding = true + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.11" + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + + implementation(libs.ui) + implementation(libs.ui.tooling) + implementation(libs.ui.tooling.preview) + implementation(libs.foundation) + implementation(libs.material) + implementation(libs.activity.ktx) + implementation(libs.activity.compose) + + //noinspection UseTomlInstead + implementation("org.simpleble.android:simpledroidble") +} \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/proguard-rules.pro b/third_party/SimpleBLE/examples/simpleble-android/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/AndroidManifest.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..ecf0890c9 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/SimpleBleAndroidExample.kt b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/SimpleBleAndroidExample.kt new file mode 100644 index 000000000..58038de5d --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/SimpleBleAndroidExample.kt @@ -0,0 +1,17 @@ +package org.simpleble.examples.android + +import android.app.Application +import android.util.Log + +class SimpleBleAndroidExample : Application() { + + override fun onCreate() { + Log.d("SimpleBleAndroidExample", "onCreate()") + super.onCreate() + } + + override fun onTerminate() { + Log.d("SimpleBleAndroidExample", "onTerminate()") + super.onTerminate() + } +} \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/activities/MainActivity.kt b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/activities/MainActivity.kt new file mode 100644 index 000000000..48a9b2e6c --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/activities/MainActivity.kt @@ -0,0 +1,136 @@ +package org.simpleble.examples.android.activities; + +import android.Manifest +import android.annotation.SuppressLint +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.BottomNavigation +import androidx.compose.material.BottomNavigationItem +import androidx.compose.material.Button +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountBox +import androidx.compose.material.icons.filled.Info +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.core.app.ActivityCompat +import org.simpleble.android.Adapter +import org.simpleble.android.SimpleDroidBle +import org.simpleble.examples.android.viewmodels.BluetoothViewModel +import org.simpleble.examples.android.views.ConnectContent +import org.simpleble.examples.android.views.ListAdaptersContent +import org.simpleble.examples.android.views.NotifyContent +import org.simpleble.examples.android.views.ReadContent +import org.simpleble.examples.android.views.ScanContent +import java.lang.ref.WeakReference + +class MainActivity : ComponentActivity() { + private val bluetoothHasPermissions = mutableStateOf(false) + private val bluetoothViewModel = BluetoothViewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Set the activity reference for SimpleDroidBle. This is required for requesting permissions. + // NOTE: This is a bit of a hacky way to do this, but it's the only way to do it for now. + SimpleDroidBle.contextReference = WeakReference(this) + bluetoothHasPermissions.value = SimpleDroidBle.hasPermissions + + setContent { + MaterialTheme { + Surface { + if (bluetoothHasPermissions.value) { + ExampleView(bluetoothViewModel) + } else { + Column { + Button(onClick = { SimpleDroidBle.requestPermissions() }) { + Text("Request Bluetooth Permissions") + } + Text("Bluetooth permissions are required to use this app") + Text("Please grant the permissions and restart the app") + } + } + } + } + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + SimpleDroidBle.handleOnRequestPermissionsResult(requestCode, permissions, grantResults) + bluetoothHasPermissions.value = SimpleDroidBle.hasPermissions + } +} + + + +@Composable +fun ExampleView(bluetoothViewModel: BluetoothViewModel) { + var selectedTab by remember { mutableIntStateOf(0) } + + Scaffold( + bottomBar = { + BottomNavigation { + BottomNavigationItem( + label = { Text("Adapter") }, + icon = { Icon(Icons.Default.Info, contentDescription = null) }, + selected = selectedTab == 0, + onClick = { selectedTab = 0 } + ) + BottomNavigationItem( + label = { Text("Scan") }, + icon = { Icon(Icons.Default.AccountBox, contentDescription = null) }, + selected = selectedTab == 1, + onClick = { selectedTab = 1 } + ) + BottomNavigationItem( + label = { Text("Connect") }, + icon = { Icon(Icons.Default.AccountBox, contentDescription = null) }, + selected = selectedTab == 2, + onClick = { selectedTab = 2 } + ) + BottomNavigationItem( + label = { Text("Read") }, + icon = { Icon(Icons.Default.AccountBox, contentDescription = null) }, + selected = selectedTab == 3, + onClick = { selectedTab = 3 } + ) + BottomNavigationItem( + label = { Text("Notify") }, + icon = { Icon(Icons.Default.AccountBox, contentDescription = null) }, + selected = selectedTab == 4, + onClick = { selectedTab = 4 } + ) + } + } + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding) + ) { + when (selectedTab) { + 0 -> ListAdaptersContent() + 1 -> ScanContent(bluetoothViewModel) + 2 -> ConnectContent(bluetoothViewModel) + 3 -> ReadContent(bluetoothViewModel) + 4 -> NotifyContent(bluetoothViewModel) + else -> ListAdaptersContent() // Default + } + } + } +} + diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/viewmodels/BluetoothViewModel.kt b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/viewmodels/BluetoothViewModel.kt new file mode 100644 index 000000000..d05ede182 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/viewmodels/BluetoothViewModel.kt @@ -0,0 +1,16 @@ +package org.simpleble.examples.android.viewmodels + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import org.simpleble.android.Adapter + +class BluetoothViewModel : ViewModel() { + private lateinit var _adapter: Adapter + val adapter: Adapter + get() { + if (!::_adapter.isInitialized) { + _adapter = Adapter.getAdapters()[0] + } + return _adapter + } +} \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/ConnectContent.kt b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/ConnectContent.kt new file mode 100644 index 000000000..c64bde9ca --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/ConnectContent.kt @@ -0,0 +1,230 @@ +package org.simpleble.examples.android.views + +import android.util.Log +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.simpleble.android.Adapter +import org.simpleble.android.BluetoothUUID +import org.simpleble.android.Peripheral +import org.simpleble.examples.android.viewmodels.BluetoothViewModel + + +@Composable +fun ConnectContent(bluetoothViewModel: BluetoothViewModel) { + var scanResults by remember { mutableStateOf(emptyList()) } + var isScanning by remember { mutableStateOf(false) } + + var selectedDevice by remember { mutableStateOf(null) } + var isConnected by remember { mutableStateOf(false) } + var mtu by remember { mutableStateOf(0) } + + LaunchedEffect(Unit, selectedDevice) { + CoroutineScope(Dispatchers.Main).launch { + bluetoothViewModel.adapter.onScanActive.collect { + isScanning = it + } + } + + CoroutineScope(Dispatchers.Main).launch { + bluetoothViewModel.adapter.onScanFound.collect { + Log.d("SimpleBLE", "Found device: ${it.identifier} [${it.address}] ${it.rssi} dBm") + scanResults = scanResults + it + } + } + + + selectedDevice?.let { peripheral -> + CoroutineScope(Dispatchers.Main).launch { + peripheral.onConnectionActive.collectLatest { active -> + isConnected = active + + mtu = if (active) { + peripheral.mtu + } else { + 0 + } + } + } + + CoroutineScope(Dispatchers.Main).launch { + peripheral.onConnected.collectLatest { + Log.d("SimpleBLE", "Connected to ${peripheral.identifier} [${peripheral.address}]") + } + } + + CoroutineScope(Dispatchers.Main).launch { + peripheral.onDisconnected.collectLatest { + Log.d("SimpleBLE", "Disconnected from ${peripheral.identifier} [${peripheral.address}]") + } + } + } + + + } + + + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + if (isScanning) { + Text( + text = "Scanning...", + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(16.dp) + ) + } + + Button( + onClick = { + if (!isScanning) { + CoroutineScope(Dispatchers.Main).launch { + scanResults = emptyList() + bluetoothViewModel.adapter.scanStart() + } + } else { + CoroutineScope(Dispatchers.Main).launch { + bluetoothViewModel.adapter.scanStop() + } + } + }, + modifier = Modifier.padding(16.dp) + ) { + Text(text = if (isScanning) "Stop Scan" else "Start Scan") + } + + selectedDevice?.let { peripheral -> + Text( + text = "Connecting to ${peripheral.identifier} [${peripheral.address}]", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(16.dp) + ) + + Button( + onClick = { + + if (!isConnected) { + CoroutineScope(Dispatchers.Main).launch { + peripheral.connect() + } + } else { + CoroutineScope(Dispatchers.Main).launch { + peripheral.disconnect() + } + } + }, + modifier = Modifier.padding(16.dp) + ) { + Text(text = if (isConnected) "Disconnect" else "Connect") + } + + if (isConnected) { + Text( + text = "Successfully connected.", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(16.dp) + ) + + Text( + text = "MTU: ${peripheral.mtu}", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(16.dp) + ) + + LazyColumn( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(8.dp) + ) { + items(peripheral.services().withIndex().toList()) { (index, service) -> + Text( + text = "Service: ${service.uuid}", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(16.dp) + ) + + service.characteristics.forEach { characteristic -> + Text( + text = "Characteristic: ${characteristic.uuid}", + style = MaterialTheme.typography.body2, + modifier = Modifier.padding(start = 32.dp) + ) + +// Text( +// text = "Capabilities: ${characteristic.capabilities.joinToString(", ")}", +// style = MaterialTheme.typography.body2, +// modifier = Modifier.padding(start = 32.dp) +// ) + + characteristic.descriptors.forEach { descriptor -> + Text( + text = "Descriptor: ${descriptor.uuid}", + style = MaterialTheme.typography.body2, + modifier = Modifier.padding(start = 48.dp) + ) + } + } + } + } + } + } + + if (scanResults.isNotEmpty()) { + Text( + text = "The following devices were found:", + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(16.dp) + ) + + LazyColumn( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(8.dp) + ) { + items(scanResults.withIndex().toList()) { (index, peripheral) -> + Text( + text = "[$index] ${peripheral.identifier} [${peripheral.address}]", + style = MaterialTheme.typography.body1, + modifier = Modifier + .padding(8.dp) + .clickable { + selectedDevice = peripheral + } + ) + } + } + } else { + Text( + text = "No devices found.", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(16.dp) + ) + } + + + } +} \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/ListAdaptersContent.kt b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/ListAdaptersContent.kt new file mode 100644 index 000000000..01dadc3a8 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/ListAdaptersContent.kt @@ -0,0 +1,88 @@ +package org.simpleble.examples.android.views + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.simpleble.android.Adapter +import org.simpleble.android.SimpleDroidBle + + +@Composable +fun ListAdaptersContent() { + var simpleBleVersion by remember { mutableStateOf("") } + var bluetoothEnabled by remember { mutableStateOf(false) } + var adapterList by remember { mutableStateOf(emptyList()) } + + LaunchedEffect(Unit) { + simpleBleVersion = SimpleDroidBle.getVersion() + bluetoothEnabled = Adapter.isBluetoothEnabled() + adapterList = Adapter.getAdapters() + } + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Using SimpleBLE version: $simpleBleVersion", + style = MaterialTheme.typography.h6, + textAlign = TextAlign.Center, + modifier = Modifier.padding(16.dp) + ) + + Text( + text = "Bluetooth enabled: $bluetoothEnabled", + style = MaterialTheme.typography.h6, + textAlign = TextAlign.Center, + modifier = Modifier.padding(16.dp) + ) + + if (adapterList.isEmpty()) { + Text( + text = "No adapter found", + style = MaterialTheme.typography.h6, + textAlign = TextAlign.Center, + modifier = Modifier.padding(16.dp) + ) + } else { + Text( + text = "Adapters:", + style = MaterialTheme.typography.h6, + textAlign = TextAlign.Center, + modifier = Modifier.padding(16.dp) + ) + + LazyColumn( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(16.dp) + ) { + items(adapterList) { adapter -> + Text( + text = "Adapter: ${adapter.identifier} [${adapter.address}]", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(8.dp) + ) + } + } + } + } +} \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/NotifyContent.kt b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/NotifyContent.kt new file mode 100644 index 000000000..a7d7ec309 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/NotifyContent.kt @@ -0,0 +1,254 @@ +package org.simpleble.examples.android.views + +import android.util.Log +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.simpleble.android.BluetoothUUID +import org.simpleble.android.Peripheral +import org.simpleble.examples.android.viewmodels.BluetoothViewModel + +@Composable +fun NotifyContent(bluetoothViewModel: BluetoothViewModel) { + var scanResults by remember { mutableStateOf(emptyList()) } + var isScanning by remember { mutableStateOf(false) } + + var selectedDevice by remember { mutableStateOf(null) } + var isConnected by remember { mutableStateOf(false) } + + var servicescharacteristics by remember { mutableStateOf(emptyList>()) } + var selectedServiceCharacteristic by remember { mutableStateOf?>(null) } + var receivedData by remember { mutableStateOf(null) } + + LaunchedEffect(Unit, selectedDevice) { + CoroutineScope(Dispatchers.Main).launch { + bluetoothViewModel.adapter.onScanActive.collect { + Log.d("SimpleBLE", "Scan active: $it") + isScanning = it + } + } + + CoroutineScope(Dispatchers.Main).launch { + bluetoothViewModel.adapter.onScanFound.collect { + Log.d("SimpleBLE", "Found device: ${it.identifier} [${it.address}] ${it.rssi} dBm") + scanResults = scanResults + it + } + } + + selectedDevice?.let { peripheral -> + CoroutineScope(Dispatchers.Main).launch { + peripheral.onConnectionActive.collectLatest { active -> + isConnected = active + } + } + + CoroutineScope(Dispatchers.Main).launch { + peripheral.onConnected.collectLatest { + Log.d("SimpleBLE", "Connected to ${peripheral.identifier} [${peripheral.address}]") + + servicescharacteristics = peripheral.services().flatMap { service -> + service.characteristics.map { characteristic -> + Log.d("SimpleBLE", "Service: ${service.uuid} Characteristic: ${characteristic.uuid} [Notify: ${characteristic.canNotify} Indicate: ${characteristic.canIndicate} Read: ${characteristic.canRead} WriteCommand: ${characteristic.canWriteCommand} WriteRequest: ${characteristic.canWriteRequest}]") + Pair(BluetoothUUID(service.uuid), BluetoothUUID(characteristic.uuid)) + } + } + } + } + + CoroutineScope(Dispatchers.Main).launch { + peripheral.onDisconnected.collectLatest { + Log.d("SimpleBLE", "Disconnected from ${peripheral.identifier} [${peripheral.address}]") + + servicescharacteristics = emptyList() + selectedServiceCharacteristic = null + receivedData = null + } + } + } + } + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + if (isScanning) { + Text( + text = "Scanning...", + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(16.dp) + ) + } + + Button( + onClick = { + if (!isScanning) { + CoroutineScope(Dispatchers.Main).launch { + scanResults = emptyList() + bluetoothViewModel.adapter.scanStart() + } + } else { + CoroutineScope(Dispatchers.Main).launch { + bluetoothViewModel.adapter.scanStop() + } + } + }, + modifier = Modifier.padding(16.dp) + ) { + Text(text = if (isScanning) "Stop Scan" else "Start Scan") + } + + + selectedDevice?.let { peripheral -> + Text( + text = "Connecting to ${peripheral.identifier} [${peripheral.address}]", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(16.dp) + ) + + Button( + onClick = { + if (!isConnected) { + CoroutineScope(Dispatchers.Main).launch { + peripheral.connect() + } + } else { + CoroutineScope(Dispatchers.Main).launch { + peripheral.disconnect() + } + } + }, + modifier = Modifier.padding(16.dp) + ) { + Text(text = if (isConnected) "Disconnect" else "Connect") + } + + if (isConnected) { + Text( + text = "Successfully connected, printing services and characteristics..", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(16.dp) + ) + + selectedServiceCharacteristic?.let { servicecharacteristic -> + Button( + onClick = { + CoroutineScope(Dispatchers.Main).launch { + peripheral.notify(servicecharacteristic.first, servicecharacteristic.second).collect { it -> + val hexString = it.joinToString(separator = " ") { "%02x".format(it) } + Log.d("SimpleBLE", "Received notification: $hexString") + } + } + CoroutineScope(Dispatchers.Main).launch { + delay(10000) + peripheral.unsubscribe(servicecharacteristic.first, servicecharacteristic.second) + } + }, + modifier = Modifier.padding(16.dp) + ) { + Text(text = "Subscribe to Notifications") + } + + Button( + onClick = { + CoroutineScope(Dispatchers.Main).launch { + peripheral.read(servicecharacteristic.first, servicecharacteristic.second) + } + }, + modifier = Modifier.padding(16.dp) + ) { + Text(text = "Read") + } + + receivedData?.let { data -> + Text( + text = "Received: ${data.joinToString(separator = " ") { "%02x".format(it) }}", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(16.dp) + ) + } + } + + LazyColumn( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(16.dp) + ) { + items(servicescharacteristics.withIndex().toList()) { (index, characteristic) -> + Text( + text = "[$index] ${characteristic.first} ${characteristic.second}", + style = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 8.sp, + lineHeight = 12.sp, + letterSpacing = 0.5.sp + ), + modifier = Modifier + .padding(8.dp) + .clickable { + selectedServiceCharacteristic = characteristic + } + ) + } + } + } + } + + if (scanResults.isNotEmpty()) { + Text( + text = "The following devices were found:", + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(16.dp) + ) + + LazyColumn( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(16.dp) + ) { + items(scanResults.withIndex().toList()) { (index, peripheral) -> + Text( + text = "[$index] ${peripheral.identifier} [${peripheral.address}]", + style = MaterialTheme.typography.body1, + modifier = Modifier + .padding(8.dp) + .clickable { + selectedDevice = peripheral + } + ) + } + } + } else { + Text( + text = "No devices found.", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(16.dp) + ) + } + } +} \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/ReadContent.kt b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/ReadContent.kt new file mode 100644 index 000000000..aeceb9ff7 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/ReadContent.kt @@ -0,0 +1,197 @@ +package org.simpleble.examples.android.views + +import android.util.Log +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.simpleble.android.Adapter +import org.simpleble.android.BluetoothUUID +import org.simpleble.android.Peripheral +import org.simpleble.examples.android.viewmodels.BluetoothViewModel + +@Composable +fun ReadContent(bluetoothViewModel: BluetoothViewModel) { + var scanResults by remember { mutableStateOf(emptyList()) } + var isScanning by remember { mutableStateOf(false) } + var selectedDevice by remember { mutableStateOf(null) } + var isConnected by remember { mutableStateOf(false) } + var characteristics by remember { mutableStateOf(emptyList>()) } + var selectedCharacteristic by remember { mutableStateOf?>(null) } + var characteristicContent by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + CoroutineScope(Dispatchers.Main).launch { + bluetoothViewModel.adapter.onScanActive.collect { + Log.d("SimpleBLE", "Scan active: $it") + isScanning = it + } + } + + CoroutineScope(Dispatchers.Main).launch { + bluetoothViewModel.adapter.onScanFound.collect { + Log.d("SimpleBLE", "Found device: ${it.identifier} [${it.address}] ${it.rssi} dBm") + scanResults = scanResults + it + } + } + } + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Button( + onClick = { + if (!isScanning) { + CoroutineScope(Dispatchers.Main).launch { + scanResults = emptyList() + bluetoothViewModel.adapter.scanFor(5000) + } + } + }, + modifier = Modifier.padding(16.dp) + ) { + Text(text = if (isScanning) "Scanning..." else "Start Scan") + } + + if (scanResults.isNotEmpty()) { + Text( + text = "The following devices were found:", + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(16.dp) + ) + + LazyColumn( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(16.dp) + ) { + items(scanResults.withIndex().toList()) { (index, peripheral) -> + Text( + text = "[$index] ${peripheral.identifier} [${peripheral.address}]", + style = MaterialTheme.typography.body1, + modifier = Modifier + .padding(8.dp) + .clickable { + selectedDevice = peripheral + } + ) + } + } + } else { + Text( + text = "No devices found.", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(16.dp) + ) + } + + selectedDevice?.let { peripheral -> + Text( + text = "Connecting to ${peripheral.identifier} [${peripheral.address}]", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(16.dp) + ) + + Button( + onClick = { + if (!isConnected) { + CoroutineScope(Dispatchers.Main).launch { + peripheral.connect() + isConnected = true + +// characteristics = peripheral.services.flatMap { service -> +// service.characteristics.map { characteristic -> +// Pair(service.uuid, characteristic.uuid) +// } +// } + } + } else { + CoroutineScope(Dispatchers.Main).launch { + peripheral.disconnect() + isConnected = false + characteristics = emptyList() + selectedCharacteristic = null + characteristicContent = null + } + } + }, + modifier = Modifier.padding(16.dp) + ) { + Text(text = if (isConnected) "Disconnect" else "Connect") + } + + if (isConnected) { + Text( + text = "Successfully connected, printing services and characteristics..", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(16.dp) + ) + + LazyColumn( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(16.dp) + ) { + items(characteristics.withIndex().toList()) { (index, characteristic) -> + Text( + text = "[$index] ${characteristic.first} ${characteristic.second}", + style = MaterialTheme.typography.body1, + modifier = Modifier + .padding(8.dp) + .clickable { + selectedCharacteristic = characteristic + } + ) + } + } + + selectedCharacteristic?.let { characteristic -> + Button( + onClick = { + CoroutineScope(Dispatchers.Main).launch { + repeat(5) { + val content = peripheral.read(characteristic.first, characteristic.second) + characteristicContent = content + Log.d("SimpleBLE", "Characteristic content: ${content.joinToString(separator = " ") { "%02x".format(it) }}") + delay(1000) + } + } + }, + modifier = Modifier.padding(16.dp) + ) { + Text(text = "Read Characteristic") + } + + characteristicContent?.let { content -> + Text( + text = "Characteristic content: ${content.joinToString(separator = " ") { "%02x".format(it) }}", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(16.dp) + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/ScanContent.kt b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/ScanContent.kt new file mode 100644 index 000000000..3253ce16c --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/java/org/simpleble/examples/android/views/ScanContent.kt @@ -0,0 +1,154 @@ +package org.simpleble.examples.android.views + +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.simpleble.android.Adapter +import org.simpleble.android.Peripheral +import org.simpleble.examples.android.viewmodels.BluetoothViewModel + + +@Composable +fun ScanContent(bluetoothViewModel: BluetoothViewModel) { + var scanActive by remember { mutableStateOf(false) } + var scanResults by remember { mutableStateOf(emptyList()) } + + LaunchedEffect(Unit) { + CoroutineScope(Dispatchers.Main).launch { + bluetoothViewModel.adapter.onScanStart.collect { + Log.d("SimpleBLE", "Scan started.") + } + } + + CoroutineScope(Dispatchers.Main).launch { + bluetoothViewModel.adapter.onScanStop.collect { + Log.d("SimpleBLE", "Scan stopped.") + } + } + + CoroutineScope(Dispatchers.Main).launch { + bluetoothViewModel.adapter.onScanActive.collect { + Log.d("SimpleBLE", "Scan active: $it") + scanActive = it + } + } + + CoroutineScope(Dispatchers.Main).launch { + bluetoothViewModel.adapter.onScanFound.collect { + Log.d("SimpleBLE", "Found device: ${it.identifier} [${it.address}] ${it.rssi} dBm ${it.hashCode()}") + scanResults = scanResults + it + } + } + + CoroutineScope(Dispatchers.Main).launch { + bluetoothViewModel.adapter.onScanUpdated.collect { + Log.d("SimpleBLE", "Updated device: ${it.identifier} [${it.address}] ${it.rssi} dBm ${it.hashCode()}") + } + } + } + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Button( + onClick = { + if (!bluetoothViewModel.adapter.scanIsActive) { + CoroutineScope(Dispatchers.Main).launch { + scanResults = emptyList() + bluetoothViewModel.adapter.scanFor(5000) + } + } + }, + modifier = Modifier.padding(16.dp) + ) { + Text(text = if (scanActive) "Scanning..." else "Start Scan") + } + + if (scanResults.isNotEmpty()) { + Text( + text = "The following devices were found:", + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(16.dp) + ) + + LazyColumn( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(16.dp) + ) { + items(scanResults.withIndex().toList()) { (index, peripheral) -> + val connectableString = if (peripheral.isConnectable) "Connectable" else "Non-Connectable" + Text( + text = "[$index] ${peripheral.identifier} [${peripheral.address}] ${peripheral.rssi} dBm $connectableString", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(8.dp) + ) + Text( + text = "Tx Power: ${peripheral.txPower} dBm", + style = MaterialTheme.typography.body2, + modifier = Modifier.padding(start = 16.dp) + ) + Text( + text = "Address Type: ${peripheral.addressType}", + style = MaterialTheme.typography.body2, + modifier = Modifier.padding(start = 16.dp) + ) + +// peripheral.services.forEach { service -> +// Text( +// text = "Service UUID: ${service.uuid}", +// style = MaterialTheme.typography.body2, +// modifier = Modifier.padding(start = 16.dp) +// ) +// Text( +// text = "Service data: ${service.data}", +// style = MaterialTheme.typography.body2, +// modifier = Modifier.padding(start = 16.dp) +// ) +// } +// +// peripheral.manufacturerData.forEach { (manufacturerId, data) -> +// Text( +// text = "Manufacturer ID: $manufacturerId", +// style = MaterialTheme.typography.body2, +// modifier = Modifier.padding(start = 16.dp) +// ) +// Text( +// text = "Manufacturer data: $data", +// style = MaterialTheme.typography.body2, +// modifier = Modifier.padding(start = 16.dp) +// ) +// } + } + } + } else { + Text( + text = "No devices found.", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(16.dp) + ) + } + } +} \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_dashboard_black_24dp.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_dashboard_black_24dp.xml new file mode 100644 index 000000000..46fc8deec --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_dashboard_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_home_black_24dp.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_home_black_24dp.xml new file mode 100644 index 000000000..f8bb0b556 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_home_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_launcher_background.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_launcher_foreground.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_notifications_black_24dp.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_notifications_black_24dp.xml new file mode 100644 index 000000000..78b75c39b --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/drawable/ic_notifications_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/layout/activity_main.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/layout/activity_main.xml new file mode 100644 index 000000000..06ea6cae2 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/layout/activity_main.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/layout/fragment_dashboard.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/layout/fragment_dashboard.xml new file mode 100644 index 000000000..166ab0e9e --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/layout/fragment_dashboard.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/layout/fragment_home.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/layout/fragment_home.xml new file mode 100644 index 000000000..f3d9b08ff --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/layout/fragment_home.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/layout/fragment_notifications.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/layout/fragment_notifications.xml new file mode 100644 index 000000000..d41793572 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/layout/fragment_notifications.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/menu/bottom_nav_menu.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/menu/bottom_nav_menu.xml new file mode 100644 index 000000000..fb6d040b9 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/menu/bottom_nav_menu.xml @@ -0,0 +1,19 @@ + + + + + + + + + + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-anydpi/ic_launcher.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-anydpi/ic_launcher_round.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-hdpi/ic_launcher.webp b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-hdpi/ic_launcher_round.webp b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9 GIT binary patch literal 2898 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 literal 0 HcmV?d00001 diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-mdpi/ic_launcher.webp b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i literal 0 HcmV?d00001 diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-xhdpi/ic_launcher.webp b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-xhdpi/ic_launcher_round.webp b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f GIT binary patch literal 3918 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�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s literal 0 HcmV?d00001 diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/navigation/mobile_navigation.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/navigation/mobile_navigation.xml new file mode 100644 index 000000000..b82e5ff58 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/navigation/mobile_navigation.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values-night/themes.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values-night/themes.xml new file mode 100644 index 000000000..8acd8e17c --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values/colors.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values/colors.xml new file mode 100644 index 000000000..f8c6127d3 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values/dimens.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values/dimens.xml new file mode 100644 index 000000000..e00c2dd14 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values/strings.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values/strings.xml new file mode 100644 index 000000000..423362190 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values/strings.xml @@ -0,0 +1,6 @@ + + SimpleBLE Example + Home + Dashboard + Notifications + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values/themes.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values/themes.xml new file mode 100644 index 000000000..3545288e1 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/xml/backup_rules.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/xml/backup_rules.xml new file mode 100644 index 000000000..fa0f996d2 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/xml/data_extraction_rules.xml b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/xml/data_extraction_rules.xml new file mode 100644 index 000000000..9ee9997b0 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/app/src/main/res_legacy/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/build.gradle.kts b/third_party/SimpleBLE/examples/simpleble-android/build.gradle.kts new file mode 100644 index 000000000..a0985efc8 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/build.gradle.kts @@ -0,0 +1,5 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.jetbrainsKotlinAndroid) apply false +} \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/gradle.properties b/third_party/SimpleBLE/examples/simpleble-android/gradle.properties new file mode 100644 index 000000000..20e2a0152 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble-android/gradle/libs.versions.toml b/third_party/SimpleBLE/examples/simpleble-android/gradle/libs.versions.toml new file mode 100644 index 000000000..59aa62cc3 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/gradle/libs.versions.toml @@ -0,0 +1,29 @@ +[versions] +agp = "8.3.1" +kotlin = "1.9.23" +coreKtx = "1.12.0" +appcompat = "1.6.1" + +ui = "1.6.4" +foundation = "1.6.4" +material = "1.6.4" +activity-ktx = "1.8.2" +activity-compose = "1.8.2" + +[libraries] +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } + +ui = { module = "androidx.compose.ui:ui", version.ref = "ui" } +ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "ui" } +ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "ui" } +foundation = { module = "androidx.compose.foundation:foundation", version.ref = "foundation" } +material = { module = "androidx.compose.material:material", version.ref = "material" } +activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activity-ktx" } +activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } + +[plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } +jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + diff --git a/third_party/SimpleBLE/examples/simpleble-android/gradle/wrapper/gradle-wrapper.jar b/third_party/SimpleBLE/examples/simpleble-android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/third_party/SimpleBLE/examples/simpleble-android/gradle/wrapper/gradle-wrapper.properties b/third_party/SimpleBLE/examples/simpleble-android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..7dc5fdd11 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Apr 27 08:22:16 PDT 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/third_party/SimpleBLE/examples/simpleble-android/gradlew b/third_party/SimpleBLE/examples/simpleble-android/gradlew new file mode 100644 index 000000000..4f906e0c8 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/third_party/SimpleBLE/examples/simpleble-android/gradlew.bat b/third_party/SimpleBLE/examples/simpleble-android/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/third_party/SimpleBLE/examples/simpleble-android/settings.gradle.kts b/third_party/SimpleBLE/examples/simpleble-android/settings.gradle.kts new file mode 100644 index 000000000..34e8a92b8 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble-android/settings.gradle.kts @@ -0,0 +1,30 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "SimpleBLE Example" + +// NOTE: This is somewhat of a hack to consume simpledroidble directly from the source code +includeBuild("../../simpledroidble") { + dependencySubstitution { + substitute(module("org.simpleble.android:simpledroidble")).using(project(":simpledroidble")) + } +} +include(":app") \ No newline at end of file diff --git a/third_party/SimpleBLE/examples/simpleble/CMakeLists.txt b/third_party/SimpleBLE/examples/simpleble/CMakeLists.txt index 280d444cb..389e3d864 100644 --- a/third_party/SimpleBLE/examples/simpleble/CMakeLists.txt +++ b/third_party/SimpleBLE/examples/simpleble/CMakeLists.txt @@ -12,20 +12,28 @@ option(SIMPLEBLE_LOCAL "Use local SimpleBLE" ON) if (SIMPLEBLE_LOCAL) add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../simpleble ${CMAKE_BINARY_DIR}/simpleble) else() + cmake_policy(SET CMP0144 OLD) # NOTE: This broke on older versions of CMake find_package(simpleble CONFIG REQUIRED) endif() # C++ add_subdirectory(cpp/list_adapters) +add_subdirectory(cpp/list_adapters_safe) add_subdirectory(cpp/scan) add_subdirectory(cpp/connect) add_subdirectory(cpp/connect_safe) +add_subdirectory(cpp/multiconnect) add_subdirectory(cpp/read) add_subdirectory(cpp/write) add_subdirectory(cpp/notify) add_subdirectory(cpp/notify_multi) # C -add_subdirectory(c/notify) -add_subdirectory(c/connect) -add_subdirectory(c/scan) +add_executable(example_connect_c c/connect.c) +target_link_libraries(example_connect_c simpleble::simpleble-c) + +add_executable(example_notify_c c/notify.c) +target_link_libraries(example_notify_c simpleble::simpleble-c) + +add_executable(example_scan_c c/scan.c) +target_link_libraries(example_scan_c simpleble::simpleble-c) diff --git a/third_party/SimpleBLE/examples/simpleble/c/connect/connect.c b/third_party/SimpleBLE/examples/simpleble/c/connect.c similarity index 100% rename from third_party/SimpleBLE/examples/simpleble/c/connect/connect.c rename to third_party/SimpleBLE/examples/simpleble/c/connect.c diff --git a/third_party/SimpleBLE/examples/simpleble/c/connect/CMakeLists.txt b/third_party/SimpleBLE/examples/simpleble/c/connect/CMakeLists.txt deleted file mode 100644 index 86add84cc..000000000 --- a/third_party/SimpleBLE/examples/simpleble/c/connect/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -cmake_minimum_required(VERSION 3.21) - -project(EXAMPLE_CONNECT_C) - -message("-- [INFO] Building Example") -add_executable(example_connect_c connect.c) -target_link_libraries(example_connect_c simpleble::simpleble-c) diff --git a/third_party/SimpleBLE/examples/simpleble/c/notify/notify.c b/third_party/SimpleBLE/examples/simpleble/c/notify.c similarity index 94% rename from third_party/SimpleBLE/examples/simpleble/c/notify/notify.c rename to third_party/SimpleBLE/examples/simpleble/c/notify.c index 300afb458..6fd7c4f84 100644 --- a/third_party/SimpleBLE/examples/simpleble/c/notify/notify.c +++ b/third_party/SimpleBLE/examples/simpleble/c/notify.c @@ -30,8 +30,8 @@ static void clean_on_exit(void); static void adapter_on_scan_start(simpleble_adapter_t adapter, void* userdata); static void adapter_on_scan_stop(simpleble_adapter_t adapter, void* userdata); static void adapter_on_scan_found(simpleble_adapter_t adapter, simpleble_peripheral_t peripheral, void* userdata); -static void peripheral_on_notify(simpleble_uuid_t service, simpleble_uuid_t characteristic, const uint8_t* data, - size_t data_length, void* userdata); +static void peripheral_on_notify(simpleble_peripheral_t handle, simpleble_uuid_t service, simpleble_uuid_t characteristic, + const uint8_t* data, size_t data_length, void* userdata); static service_characteristic_t characteristic_list[SERVICES_LIST_SIZE] = {0}; static simpleble_peripheral_t peripheral_list[PERIPHERAL_LIST_SIZE] = {0}; @@ -205,8 +205,8 @@ static void adapter_on_scan_found(simpleble_adapter_t adapter, simpleble_periphe simpleble_free(peripheral_address); } -static void peripheral_on_notify(simpleble_uuid_t service, simpleble_uuid_t characteristic, const uint8_t* data, - size_t data_length, void* userdata) { +static void peripheral_on_notify(simpleble_peripheral_t handle, simpleble_uuid_t service, simpleble_uuid_t characteristic, + const uint8_t* data, size_t data_length, void* userdata) { printf("Received: "); for (size_t i = 0; i < data_length; i++) { printf("%02X ", data[i]); diff --git a/third_party/SimpleBLE/examples/simpleble/c/notify/CMakeLists.txt b/third_party/SimpleBLE/examples/simpleble/c/notify/CMakeLists.txt deleted file mode 100644 index 7509a2b95..000000000 --- a/third_party/SimpleBLE/examples/simpleble/c/notify/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -cmake_minimum_required(VERSION 3.21) - -project(EXAMPLE_NOTIFY_C) - -message("-- [INFO] Building Example") -add_executable(example_notify_c notify.c) -target_link_libraries(example_notify_c simpleble::simpleble-c) diff --git a/third_party/SimpleBLE/examples/simpleble/c/scan/scan.c b/third_party/SimpleBLE/examples/simpleble/c/scan.c similarity index 63% rename from third_party/SimpleBLE/examples/simpleble/c/scan/scan.c rename to third_party/SimpleBLE/examples/simpleble/c/scan.c index 5626631d2..7d7f9a970 100644 --- a/third_party/SimpleBLE/examples/simpleble/c/scan/scan.c +++ b/third_party/SimpleBLE/examples/simpleble/c/scan.c @@ -10,6 +10,20 @@ #include "simpleble_c/simpleble.h" +static void print_buffer_hex(uint8_t* buf, size_t len, bool newline) { + for (size_t i = 0; i < len; i++) { + printf("%02X", buf[i]); + + if (i < (len - 1)) { + printf(" "); + } + } + + if (newline) { + printf("\n"); + } +} + static void adapter_on_scan_start(simpleble_adapter_t adapter, void* userdata); static void adapter_on_scan_stop(simpleble_adapter_t adapter, void* userdata); static void adapter_on_scan_found(simpleble_adapter_t adapter, simpleble_peripheral_t peripheral, void* userdata); @@ -43,6 +57,46 @@ int main() { // internal peripheral took longer to stop than anticipated. SLEEP_SEC(1); + size_t peripheral_count = simpleble_adapter_scan_get_results_count(adapter); + for (size_t peripheral_index = 0; peripheral_index < peripheral_count; peripheral_index++) { + simpleble_peripheral_t peripheral = simpleble_adapter_scan_get_results_handle(adapter, peripheral_index); + + char* peripheral_identifier = simpleble_peripheral_identifier(peripheral); + char* peripheral_address = simpleble_peripheral_address(peripheral); + + bool peripheral_connectable = false; + simpleble_peripheral_is_connectable(peripheral, &peripheral_connectable); + + int16_t peripheral_rssi = simpleble_peripheral_rssi(peripheral); + + printf("[%zu] %s [%s] %d dBm %s\n", peripheral_index, peripheral_identifier, peripheral_address, + peripheral_rssi, peripheral_connectable ? "Connectable" : "Non-Connectable"); + + size_t services_count = simpleble_peripheral_services_count(peripheral); + for (size_t service_index = 0; service_index < services_count; service_index++) { + simpleble_service_t service; + simpleble_peripheral_services_get(peripheral, service_index, &service); + + printf(" Service UUID: %s\n", service.uuid.value); + printf(" Service data: "); + print_buffer_hex(service.data, service.data_length, true); + } + + size_t manufacturer_data_count = simpleble_peripheral_manufacturer_data_count(peripheral); + for (size_t manuf_data_index = 0; manuf_data_index < manufacturer_data_count; manuf_data_index++) { + simpleble_manufacturer_data_t manuf_data; + simpleble_peripheral_manufacturer_data_get(peripheral, manuf_data_index, &manuf_data); + printf(" Manufacturer ID: %04X\n", manuf_data.manufacturer_id); + printf(" Manufacturer data: "); + print_buffer_hex(manuf_data.data, manuf_data.data_length, true); + } + + // Let's not forget to release the associated handles and memory + simpleble_peripheral_release_handle(peripheral); + simpleble_free(peripheral_address); + simpleble_free(peripheral_identifier); + } + // Let's not forget to release the associated handle. simpleble_adapter_release_handle(adapter); diff --git a/third_party/SimpleBLE/examples/simpleble/c/scan/CMakeLists.txt b/third_party/SimpleBLE/examples/simpleble/c/scan/CMakeLists.txt deleted file mode 100644 index bbd7bc5f2..000000000 --- a/third_party/SimpleBLE/examples/simpleble/c/scan/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -cmake_minimum_required(VERSION 3.21) - -project(EXAMPLE_SCAN_C) - -message("-- [INFO] Building Example") -add_executable(example_scan_c scan.c) -target_link_libraries(example_scan_c simpleble::simpleble-c) diff --git a/third_party/SimpleBLE/examples/simpleble/cpp/common/utils.cpp b/third_party/SimpleBLE/examples/simpleble/cpp/common/utils.cpp index 4304cd389..1173e213d 100644 --- a/third_party/SimpleBLE/examples/simpleble/cpp/common/utils.cpp +++ b/third_party/SimpleBLE/examples/simpleble/cpp/common/utils.cpp @@ -3,13 +3,6 @@ #include #include -void Utils::print_byte_array(const SimpleBLE::ByteArray& bytes) { - for (auto b : bytes) { - std::cout << std::hex << std::setfill('0') << std::setw(2) << (uint32_t)((uint8_t)b) << " "; - } - std::cout << std::endl; -} - std::optional Utils::getUserInputInt(const std::string& line, std::size_t max) { std::size_t ret; diff --git a/third_party/SimpleBLE/examples/simpleble/cpp/common/utils.hpp b/third_party/SimpleBLE/examples/simpleble/cpp/common/utils.hpp index ba84bea74..b9ccda754 100644 --- a/third_party/SimpleBLE/examples/simpleble/cpp/common/utils.hpp +++ b/third_party/SimpleBLE/examples/simpleble/cpp/common/utils.hpp @@ -22,10 +22,6 @@ std::optional getAdapter(); */ std::optional getUserInputInt(const std::string& line, std::size_t max); -/** - * @brief Pretty print a ByteArray - */ -void print_byte_array(const SimpleBLE::ByteArray& bytes); } // namespace Utils #endif diff --git a/third_party/SimpleBLE/examples/simpleble/cpp/connect/connect.cpp b/third_party/SimpleBLE/examples/simpleble/cpp/connect/connect.cpp index 5f1fa4d43..92ca530af 100644 --- a/third_party/SimpleBLE/examples/simpleble/cpp/connect/connect.cpp +++ b/third_party/SimpleBLE/examples/simpleble/cpp/connect/connect.cpp @@ -16,14 +16,19 @@ int main() { std::vector peripherals; - adapter.set_callback_on_scan_found([&](SimpleBLE::Peripheral peripheral) { peripherals.push_back(peripheral); }); + adapter.set_callback_on_scan_found([&](SimpleBLE::Peripheral peripheral) { + std::cout << "Found device: " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl; + if (peripheral.is_connectable()) { + peripherals.push_back(peripheral); + } + }); adapter.set_callback_on_scan_start([]() { std::cout << "Scan started." << std::endl; }); adapter.set_callback_on_scan_stop([]() { std::cout << "Scan stopped." << std::endl; }); // Scan for 5 seconds and return. adapter.scan_for(5000); - std::cout << "The following devices were found:" << std::endl; + std::cout << "The following connectable devices were found:" << std::endl; for (size_t i = 0; i < peripherals.size(); i++) { std::cout << "[" << i << "] " << peripherals[i].identifier() << " [" << peripherals[i].address() << "]" << std::endl; diff --git a/third_party/SimpleBLE/examples/simpleble/cpp/connect_safe/connect_safe.cpp b/third_party/SimpleBLE/examples/simpleble/cpp/connect_safe/connect_safe.cpp index 965a6c989..a08e41d2c 100644 --- a/third_party/SimpleBLE/examples/simpleble/cpp/connect_safe/connect_safe.cpp +++ b/third_party/SimpleBLE/examples/simpleble/cpp/connect_safe/connect_safe.cpp @@ -9,7 +9,7 @@ int main() { auto adapter_list = SimpleBLE::Safe::Adapter::get_adapters(); if (!adapter_list.has_value()) { - std::cout << "Failed to " << std::endl; + std::cout << "Failed to list adapters" << std::endl; return EXIT_FAILURE; } diff --git a/third_party/SimpleBLE/examples/simpleble/cpp/list_adapters/list_adapters.cpp b/third_party/SimpleBLE/examples/simpleble/cpp/list_adapters/list_adapters.cpp index 33d9c1f9e..acfec00c6 100644 --- a/third_party/SimpleBLE/examples/simpleble/cpp/list_adapters/list_adapters.cpp +++ b/third_party/SimpleBLE/examples/simpleble/cpp/list_adapters/list_adapters.cpp @@ -1,8 +1,10 @@ #include #include "simpleble/Adapter.h" +#include "simpleble/Utils.h" int main() { + std::cout << "Using SimpleBLE version: " << SimpleBLE::get_simpleble_version() << std::endl; std::cout << "Bluetooth enabled: " << SimpleBLE::Adapter::bluetooth_enabled() << std::endl; auto adapter_list = SimpleBLE::Adapter::get_adapters(); @@ -14,8 +16,6 @@ int main() { for (auto& adapter : adapter_list) { std::cout << "Adapter: " << adapter.identifier() << " [" << adapter.address() << "]" << std::endl; - - adapter.underlying(); } return EXIT_SUCCESS; diff --git a/third_party/SimpleBLE/examples/simpleble/cpp/list_adapters_safe/CMakeLists.txt b/third_party/SimpleBLE/examples/simpleble/cpp/list_adapters_safe/CMakeLists.txt new file mode 100644 index 000000000..0b19c189e --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble/cpp/list_adapters_safe/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.21) + +project(EXAMPLE_LIST_ADAPTERS_SAFE) + +message("-- [INFO] Building Example") + +add_executable(example_list_adapters_safe + list_adapters_safe.cpp +) + +target_link_libraries(example_list_adapters_safe simpleble::simpleble) diff --git a/third_party/SimpleBLE/examples/simpleble/cpp/list_adapters_safe/list_adapters_safe.cpp b/third_party/SimpleBLE/examples/simpleble/cpp/list_adapters_safe/list_adapters_safe.cpp new file mode 100644 index 000000000..ca6a0399e --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble/cpp/list_adapters_safe/list_adapters_safe.cpp @@ -0,0 +1,33 @@ +#include + +#include "simpleble/AdapterSafe.h" + +int main() { + auto bluetooth_enabled = SimpleBLE::Safe::Adapter::bluetooth_enabled(); + + if (!bluetooth_enabled.has_value()) { + std::cout << "Failed to determine Bluetooth status" << std::endl; + return EXIT_FAILURE; + } + + std::cout << "Bluetooth enabled: " << bluetooth_enabled.value() << std::endl; + + auto adapter_list = SimpleBLE::Safe::Adapter::get_adapters(); + + if (!adapter_list.has_value()) { + std::cout << "Failed to list adapters" << std::endl; + return EXIT_FAILURE; + } + + if (adapter_list->empty()) { + std::cout << "No adapter found" << std::endl; + return EXIT_FAILURE; + } + + for (auto& adapter : *adapter_list) { + std::cout << "Adapter: " << adapter.identifier().value() << " [" << adapter.address().value() << "]" + << std::endl; + } + + return EXIT_SUCCESS; +} diff --git a/third_party/SimpleBLE/examples/simpleble/cpp/multiconnect/CMakeLists.txt b/third_party/SimpleBLE/examples/simpleble/cpp/multiconnect/CMakeLists.txt new file mode 100644 index 000000000..c49cfcc4a --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble/cpp/multiconnect/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.21) + +project(EXAMPLE_MULTICONNECT) + +message("-- [INFO] Building Example") + +add_executable(example_multiconnect + multiconnect.cpp + ../common/utils.cpp +) + +target_link_libraries(example_multiconnect simpleble::simpleble) diff --git a/third_party/SimpleBLE/examples/simpleble/cpp/multiconnect/multiconnect.cpp b/third_party/SimpleBLE/examples/simpleble/cpp/multiconnect/multiconnect.cpp new file mode 100644 index 000000000..5da268993 --- /dev/null +++ b/third_party/SimpleBLE/examples/simpleble/cpp/multiconnect/multiconnect.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include + +#include "../common/utils.hpp" + +#include "simpleble/SimpleBLE.h" + +using namespace std::chrono_literals; + +int main() { + auto adapter_optional = Utils::getAdapter(); + + if (!adapter_optional.has_value()) { + return EXIT_FAILURE; + } + + auto adapter = adapter_optional.value(); + + std::vector peripherals; + + adapter.set_callback_on_scan_found([&](SimpleBLE::Peripheral peripheral) { peripherals.push_back(peripheral); }); + + // Scan for 5 seconds and return. + adapter.scan_for(5000); + + std::cout << "The following devices were found:" << std::endl; + for (size_t i = 0; i < peripherals.size(); i++) { + std::cout << "[" << i << "] " << peripherals[i].identifier() << " [" << peripherals[i].address() << "]" + << std::endl; + } + + auto selection = Utils::getUserInputInt("Please select a device to connect to", peripherals.size() - 1); + if (!selection.has_value()) { + return EXIT_FAILURE; + } + + auto peripheral = peripherals[selection.value()]; + peripheral.set_callback_on_connected([]() { std::cout << "Connected callback triggered" << std::endl; }); + peripheral.set_callback_on_disconnected([]() { std::cout << "Disconnected callback triggered" << std::endl; }); + + std::cout << "Connecting to " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl; + + for (size_t i = 0; i < 5; i++) { + try { + peripheral.connect(); + std::cout << "Successfully connected." << std::endl; + std::this_thread::sleep_for(2s); + + peripheral.disconnect(); + std::cout << "Successfully disconnected." << std::endl; + } catch (const std::exception& ex) { + std::cout << "Failed at " << i << " with: " << ex.what() << std::endl; + throw; + } + } + + return EXIT_SUCCESS; +} diff --git a/third_party/SimpleBLE/examples/simpleble/cpp/notify/notify.cpp b/third_party/SimpleBLE/examples/simpleble/cpp/notify/notify.cpp index 8937e1f13..8fa10cf24 100644 --- a/third_party/SimpleBLE/examples/simpleble/cpp/notify/notify.cpp +++ b/third_party/SimpleBLE/examples/simpleble/cpp/notify/notify.cpp @@ -23,7 +23,9 @@ int main() { adapter.set_callback_on_scan_found([&](SimpleBLE::Peripheral peripheral) { std::cout << "Found device: " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl; - peripherals.push_back(peripheral); + if (peripheral.is_connectable()) { + peripherals.push_back(peripheral); + } }); adapter.set_callback_on_scan_start([]() { std::cout << "Scan started." << std::endl; }); @@ -68,10 +70,8 @@ int main() { } // Subscribe to the characteristic. - peripheral.notify(uuids[selection.value()].first, uuids[selection.value()].second, [&](SimpleBLE::ByteArray bytes) { - std::cout << "Received: "; - Utils::print_byte_array(bytes); - }); + peripheral.notify(uuids[selection.value()].first, uuids[selection.value()].second, + [&](SimpleBLE::ByteArray bytes) { std::cout << "Received: " << bytes << std::endl; }); std::this_thread::sleep_for(5s); diff --git a/third_party/SimpleBLE/examples/simpleble/cpp/notify_multi/notify_multi.cpp b/third_party/SimpleBLE/examples/simpleble/cpp/notify_multi/notify_multi.cpp index 88258e0ff..dfe366507 100644 --- a/third_party/SimpleBLE/examples/simpleble/cpp/notify_multi/notify_multi.cpp +++ b/third_party/SimpleBLE/examples/simpleble/cpp/notify_multi/notify_multi.cpp @@ -81,8 +81,7 @@ int main() { peripherals[iter].notify(uuids[selection.value()].first, uuids[selection.value()].second, [&, iter](SimpleBLE::ByteArray bytes) { if (print_allowed) { - std::cout << "Peripheral " << iter << " received: "; - Utils::print_byte_array(bytes); + std::cout << "Peripheral " << iter << " received: " << bytes << std::endl; } }); } diff --git a/third_party/SimpleBLE/examples/simpleble/cpp/read/read.cpp b/third_party/SimpleBLE/examples/simpleble/cpp/read/read.cpp index 14aa576fd..3406d856d 100644 --- a/third_party/SimpleBLE/examples/simpleble/cpp/read/read.cpp +++ b/third_party/SimpleBLE/examples/simpleble/cpp/read/read.cpp @@ -22,7 +22,9 @@ int main() { adapter.set_callback_on_scan_found([&](SimpleBLE::Peripheral peripheral) { std::cout << "Found device: " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl; - peripherals.push_back(peripheral); + if (peripheral.is_connectable()) { + peripherals.push_back(peripheral); + } }); adapter.set_callback_on_scan_start([]() { std::cout << "Scan started." << std::endl; }); @@ -70,8 +72,7 @@ int main() { // Attempt to read the characteristic 5 times in 5 seconds. for (size_t i = 0; i < 5; i++) { SimpleBLE::ByteArray rx_data = peripheral.read(uuids[selection.value()].first, uuids[selection.value()].second); - std::cout << "Characteristic content is: "; - Utils::print_byte_array(rx_data); + std::cout << "Characteristic content is: " << rx_data << std::endl; std::this_thread::sleep_for(1s); } diff --git a/third_party/SimpleBLE/examples/simpleble/cpp/scan/scan.cpp b/third_party/SimpleBLE/examples/simpleble/cpp/scan/scan.cpp index d4fb30a22..baae68ffc 100644 --- a/third_party/SimpleBLE/examples/simpleble/cpp/scan/scan.cpp +++ b/third_party/SimpleBLE/examples/simpleble/cpp/scan/scan.cpp @@ -26,7 +26,9 @@ int main() { }); adapter.set_callback_on_scan_start([]() { std::cout << "Scan started." << std::endl; }); + adapter.set_callback_on_scan_stop([]() { std::cout << "Scan stopped." << std::endl; }); + // Scan for 5 seconds. adapter.scan_for(5000); @@ -36,20 +38,24 @@ int main() { std::cout << "The following devices were found:" << std::endl; for (size_t i = 0; i < peripherals.size(); i++) { std::string connectable_string = peripherals[i].is_connectable() ? "Connectable" : "Non-Connectable"; - std::string peripheral_string = peripherals[i].identifier() + " [" + peripherals[i].address() + "]"; + std::string peripheral_string = peripherals[i].identifier() + " [" + peripherals[i].address() + "] " + + std::to_string(peripherals[i].rssi()) + " dBm"; std::cout << "[" << i << "] " << peripheral_string << " " << connectable_string << std::endl; + std::cout << " Tx Power: " << std::dec << peripherals[i].tx_power() << " dBm" << std::endl; + std::cout << " Address Type: " << peripherals[i].address_type() << std::endl; + std::vector services = peripherals[i].services(); for (auto& service : services) { - std::cout << " Service: " << service.uuid() << std::endl; + std::cout << " Service UUID: " << service.uuid() << std::endl; + std::cout << " Service data: " << service.data() << std::endl; } std::map manufacturer_data = peripherals[i].manufacturer_data(); for (auto& [manufacturer_id, data] : manufacturer_data) { std::cout << " Manufacturer ID: " << manufacturer_id << std::endl; - std::cout << " Manufacturer data: "; - Utils::print_byte_array(data); + std::cout << " Manufacturer data: " << data << std::endl; } } return EXIT_SUCCESS; diff --git a/third_party/SimpleBLE/examples/simpleble/cpp/write/write.cpp b/third_party/SimpleBLE/examples/simpleble/cpp/write/write.cpp index 443c4eacd..2c3b9394e 100644 --- a/third_party/SimpleBLE/examples/simpleble/cpp/write/write.cpp +++ b/third_party/SimpleBLE/examples/simpleble/cpp/write/write.cpp @@ -19,7 +19,9 @@ int main() { adapter.set_callback_on_scan_found([&](SimpleBLE::Peripheral peripheral) { std::cout << "Found device: " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl; - peripherals.push_back(peripheral); + if (peripheral.is_connectable()) { + peripherals.push_back(peripheral); + } }); adapter.set_callback_on_scan_start([]() { std::cout << "Scan started." << std::endl; }); @@ -67,10 +69,12 @@ int main() { return EXIT_FAILURE; } + SimpleBLE::ByteArray bytes = SimpleBLE::ByteArray::fromHex(contents); + // NOTE: Alternatively, `write_command` can be used to write to a characteristic too. // `write_request` is for unacknowledged writes. // `write_command` is for acknowledged writes. - peripheral.write_request(uuids[selection.value()].first, uuids[selection.value()].second, contents); + peripheral.write_request(uuids[selection.value()].first, uuids[selection.value()].second, bytes); peripheral.disconnect(); return EXIT_SUCCESS; diff --git a/third_party/SimpleBLE/examples/simplebluez/scan/scan.cpp b/third_party/SimpleBLE/examples/simplebluez/scan/scan.cpp index 322ba659d..be45bf8d7 100644 --- a/third_party/SimpleBLE/examples/simplebluez/scan/scan.cpp +++ b/third_party/SimpleBLE/examples/simplebluez/scan/scan.cpp @@ -53,7 +53,9 @@ int main(int argc, char* argv[]) { adapter->set_on_device_updated([](std::shared_ptr device) { std::cout << "Update received for " << device->address() << std::endl; std::cout << "\tName " << device->name() << std::endl; + std::cout << "\tAddress Type " << device->address_type() << std::endl; std::cout << "\tRSSI " << std::dec << device->rssi() << std::endl; + std::cout << "\tTxPower " << std::dec << device->tx_power() << std::endl; auto manufacturer_data = device->manufacturer_data(); for (auto& [manufacturer_id, value_array] : manufacturer_data) { std::cout << "\tManuf ID 0x" << std::setfill('0') << std::setw(4) << std::hex << (int)manufacturer_id; diff --git a/third_party/SimpleBLE/examples/simplepyble/connect.py b/third_party/SimpleBLE/examples/simplepyble/connect.py index 78ef3e515..9cad11d18 100644 --- a/third_party/SimpleBLE/examples/simplepyble/connect.py +++ b/third_party/SimpleBLE/examples/simplepyble/connect.py @@ -42,4 +42,7 @@ for characteristic in service.characteristics(): print(f" Characteristic: {characteristic.uuid()}") + capabilities = " ".join(characteristic.capabilities()) + print(f" Capabilities: {capabilities}") + peripheral.disconnect() diff --git a/third_party/SimpleBLE/examples/simplepyble/list_adapters.py b/third_party/SimpleBLE/examples/simplepyble/list_adapters.py index 595b0573d..f7bf14246 100644 --- a/third_party/SimpleBLE/examples/simplepyble/list_adapters.py +++ b/third_party/SimpleBLE/examples/simplepyble/list_adapters.py @@ -1,6 +1,8 @@ import simplepyble if __name__ == "__main__": + print(f"Running on {simplepyble.get_operating_system()}") + adapters = simplepyble.Adapter.get_adapters() if len(adapters) == 0: diff --git a/third_party/SimpleBLE/examples/simplepyble/scan.py b/third_party/SimpleBLE/examples/simplepyble/scan.py index e152ad968..8c7a2c3eb 100644 --- a/third_party/SimpleBLE/examples/simplepyble/scan.py +++ b/third_party/SimpleBLE/examples/simplepyble/scan.py @@ -28,8 +28,15 @@ for peripheral in peripherals: connectable_str = "Connectable" if peripheral.is_connectable() else "Non-Connectable" print(f"{peripheral.identifier()} [{peripheral.address()}] - {connectable_str}") + print(f' Address Type: {peripheral.address_type()}') + print(f' Tx Power: {peripheral.tx_power()} dBm') manufacturer_data = peripheral.manufacturer_data() for manufacturer_id, value in manufacturer_data.items(): print(f" Manufacturer ID: {manufacturer_id}") print(f" Manufacturer data: {value}") + + services = peripheral.services() + for service in services: + print(f" Service UUID: {service.uuid()}") + print(f" Service data: {service.data()}") diff --git a/third_party/SimpleBLE/examples/simplersble/Cargo.lock b/third_party/SimpleBLE/examples/simplersble/Cargo.lock new file mode 100644 index 000000000..e4ac13503 --- /dev/null +++ b/third_party/SimpleBLE/examples/simplersble/Cargo.lock @@ -0,0 +1,190 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cmake" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c" +dependencies = [ + "cc", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "cxx" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc831ee6a32dd495436e317595e639a587aa9907bef96fe6e6abc290ab6204e9" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94331d54f1b1a8895cd81049f7eaaaef9d05a7dcb4d1fd08bf3ff0806246789d" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dcd35ba14ca9b40d6e4b4b39961f23d835dbb8eed74565ded361d93e1feb8a" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bbeb29798b407ccd82a3324ade1a7286e0d29851475990b612670f6f5124d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "proc-macro2" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + +[[package]] +name = "simplersble" +version = "0.6.0" +dependencies = [ + "cmake", + "cxx", + "cxx-build", +] + +[[package]] +name = "simplersble-examples" +version = "0.0.0" +dependencies = [ + "simplersble", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/third_party/SimpleBLE/examples/simplersble/Cargo.toml b/third_party/SimpleBLE/examples/simplersble/Cargo.toml new file mode 100644 index 000000000..fd9a57907 --- /dev/null +++ b/third_party/SimpleBLE/examples/simplersble/Cargo.toml @@ -0,0 +1,19 @@ +[package] +edition = "2021" +name = "simplersble-examples" +version = "0.0.0" + +[[bin]] +name = "list_adapters" + +[[bin]] +name = "scan" + +[[bin]] +name = "connect" + +[[bin]] +name = "notify" + +[dependencies] +simplersble = { path = "../.." } \ No newline at end of file diff --git a/third_party/SimpleBLE/external/include/external/kvn_bytearray.h b/third_party/SimpleBLE/external/include/external/kvn_bytearray.h new file mode 100644 index 000000000..ac1fa991d --- /dev/null +++ b/third_party/SimpleBLE/external/include/external/kvn_bytearray.h @@ -0,0 +1,200 @@ +#ifndef KVN_BYTEARRAY_H +#define KVN_BYTEARRAY_H + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace kvn { + +/** + * @class bytearray + * @brief A class to handle byte arrays and their conversion from/to hex strings. + */ +class bytearray { + public: + /** + * @brief Default constructor. + */ + bytearray() = default; + + /** + * @brief Constructs byte array from a vector of uint8_t. + * @param vec A vector of uint8_t. + */ + bytearray(const std::vector& vec) : data_(vec) {} + + /** + * @brief Constructs byte array from a raw pointer and size. + * @param ptr A pointer to uint8_t data. + * @param size The size of the data. + */ + bytearray(const uint8_t* ptr, size_t size) : data_(ptr, ptr + size) {} + + /** + * @brief Constructs byte array from a std::string. + * @param byteArr A string containing byte data. + */ + bytearray(const std::string& byteArr) : data_(byteArr.begin(), byteArr.end()) {} + + /** + * @brief Constructs byte array from a C-style string and size. + * @param byteArr A C-style string. + * @param size The size of the string. + */ + bytearray(const char* byteArr, size_t size) : bytearray(std::string(byteArr, size)) {} + + /** + * @brief Constructs byte array from a C-style string. + * @param byteArr A C-style string. + */ + bytearray(const char* byteArr) : bytearray(std::string(byteArr)) {} + + /** + * @brief Creates a ByteArray from a hex string. + * + * Case is ignored and the string may have a '0x' hex prefix or not. + * + * @param hexStr A string containing hex data. + * @return A ByteArray object. + * @throws std::invalid_argument If the hex string contains non-hexadecimal characters. + * @throws std::length_error If the hex string length is not even. + */ + static bytearray fromHex(const std::string& hexStr) { + std::string cleanString(hexStr); + + // Check and skip the '0x' prefix if present + if (cleanString.size() >= 2 && cleanString.substr(0, 2) == "0x") { + cleanString = cleanString.substr(2); + } + + size_t size = cleanString.size(); + if (size % 2 != 0) { + throw std::length_error("Hex string length must be even."); + } + + bytearray byteArray; + byteArray.data_.reserve(size / 2); + + for (size_t i = 0; i < size; i += 2) { + uint8_t byte = static_cast(std::stoi(cleanString.substr(i, 2), nullptr, 16)); + byteArray.data_.push_back(byte); + } + + return byteArray; + } + + /** + * @overload + */ + static bytearray fromHex(const char* byteArr) { return fromHex(std::string(byteArr)); } + + /** + * @overload + */ + static bytearray fromHex(const char* byteArr, size_t size) { return fromHex(std::string(byteArr, size)); } + + /** + * @brief Converts the byte array to a lowercase hex string without '0x' prefix. + * @param spacing Whether to include spaces between bytes. + * + * @return A hex string representation of the byte array. + */ + std::string toHex(bool spacing = false) const { + std::ostringstream oss; + for (auto byte : data_) { + oss << std::hex << std::setw(2) << std::setfill('0') << static_cast(byte); + if (spacing) { + oss << " "; + } + } + return oss.str(); + } + + /** + * @brief Slices the byte array from a specified start index to an end index. + * + * This method creates a new byte array containing bytes from the specified range. + * The start index is inclusive, while the end index is exclusive. + * + * @param start The starting index from which to begin slicing. + * @param end The ending index up to which to slice (exclusive). + * @return byte array A new byte array containing the sliced segment. + * @throws std::out_of_range If the start index is greater than the end index or if the end index is out of bounds. + */ + bytearray slice(size_t start, size_t end) const { + if (start > end || end > data_.size()) { + throw std::out_of_range("Invalid slice range"); + } + return bytearray(std::vector(data_.begin() + start, data_.begin() + end)); + } + + /** + * @brief Slices the byte array from a specified start index to the end of the array. + * + * This method creates a new byte array containing all bytes from the specified start index to the end of the + * byte array. + * + * @param start The starting index from which to begin slicing. + * @return byte array A new byte array containing the sliced segment from the start index to the end. + * @throws std::out_of_range If the start index is out of the bounds of the byte array. + */ + bytearray slice_from(size_t start) const { return slice(start, data_.size()); } + + /** + * @brief Slices the byte array from the beginning to a specified end index. + * + * This method creates a new byte array containing all bytes from the beginning of the byte array to the specified + * end index. + * + * @param end The ending index up to which to slice (exclusive). + * @return byte array A new byte array containing the sliced segment from the beginning to the end index. + * @throws std::out_of_range If the end index is out of the bounds of the byte array. + */ + bytearray slice_to(size_t end) const { return slice(0, end); } + + /** + * @brief Overloaded stream insertion operator for byte array. + * @param os The output stream. + * @param byteArray The byte array object. + * @return The output stream. + */ + friend std::ostream& operator<<(std::ostream& os, const bytearray& byteArray) { + os << byteArray.toHex(true); + return os; + } + + /** + * @brief Conversion operator to convert byte array to std::string. + * + * @note This is provided to support code that relies on byte array + * being representd as a string. + * @return String containing the raw bytes of the byte array + */ + operator std::string() const { return std::string(data_.begin(), data_.end()); } + + //! @cond Doxygen_Suppress + // Expose vector-like functionality + size_t size() const { return data_.size(); } + const uint8_t* data() const { return data_.data(); } + bool empty() const { return data_.empty(); } + void clear() { data_.clear(); } + uint8_t& operator[](size_t index) { return data_[index]; } + const uint8_t& operator[](size_t index) const { return data_[index]; } + void push_back(uint8_t byte) { data_.push_back(byte); } + auto begin() const { return data_.begin(); } + auto end() const { return data_.end(); } + //! @endcond + + private: + std::vector data_; +}; + +} // namespace kvn + +#endif // KVN_BYTEARRAY_H \ No newline at end of file diff --git a/third_party/SimpleBLE/simplepyble/pyproject.toml b/third_party/SimpleBLE/pyproject.toml similarity index 55% rename from third_party/SimpleBLE/simplepyble/pyproject.toml rename to third_party/SimpleBLE/pyproject.toml index 0d8138c90..33ef92d8f 100644 --- a/third_party/SimpleBLE/simplepyble/pyproject.toml +++ b/third_party/SimpleBLE/pyproject.toml @@ -1,8 +1,8 @@ [build-system] requires = [ - "setuptools", - "wheel", - "ninja", + "setuptools>=42", + "scikit-build", + "ninja; platform_system!='Windows'", "cmake>=3.21", "pybind11", ] diff --git a/third_party/SimpleBLE/simplepyble/setup.py b/third_party/SimpleBLE/setup.py similarity index 54% rename from third_party/SimpleBLE/simplepyble/setup.py rename to third_party/SimpleBLE/setup.py index 2dd35991a..db3ca3d9a 100644 --- a/third_party/SimpleBLE/simplepyble/setup.py +++ b/third_party/SimpleBLE/setup.py @@ -1,30 +1,45 @@ +import argparse +import os import pathlib import sys -import setuptools -import argparse + +import pybind11 +import skbuild + + +def exclude_unnecessary_files(cmake_manifest): + def is_necessary(name): + is_necessary = ( + name.endswith(".so") + or name.endswith(".dylib") + or name.endswith("py") + or name.endswith("pyd") + ) + print(f"Parsing file: {name} - {is_necessary}") + return is_necessary + + return list(filter(is_necessary, cmake_manifest)) + argparser = argparse.ArgumentParser(add_help=False) -argparser.add_argument('--plain', help='Use Plain SimpleBLE', required=False, action='store_true') +argparser.add_argument( + "--plain", help="Use Plain SimpleBLE", required=False, action="store_true" +) args, unknown = argparser.parse_known_args() sys.argv = [sys.argv[0]] + unknown -here = pathlib.Path(__file__).parent.resolve() -root = here.parent.resolve() - -# Include our vendorized copy of cmake-build-extension, at least until -# https://github.com/diegoferigo/cmake-build-extension/pull/35 is merged. -sys.path.insert(0, str(here)) -import cmake_build_extension +root = pathlib.Path(__file__).parent.resolve() # Generate the version string # TODO: Make the dev portion smarter by looking at tags. version_str = (root / "VERSION").read_text(encoding="utf-8").strip() -version_str += ".dev0" # ! Ensure it matches the intended release version! +version_str += ".dev0" # ! Ensure it matches the intended release version! # Get the long description from the README file -long_description = (here / "README.rst").read_text(encoding="utf-8") +long_description = (root / "simplepyble" / "README.rst").read_text(encoding="utf-8") cmake_options = [] +cmake_options.append(f"-Dpybind11_DIR={pybind11.get_cmake_dir()}") if sys.platform == "win32": cmake_options.append("-DCMAKE_SYSTEM_VERSION=10.0.19041.0") elif sys.platform.startswith("darwin"): @@ -35,9 +50,12 @@ if args.plain: cmake_options.append("-DSIMPLEBLE_PLAIN=ON") +if 'PIWHEELS_BUILD' in os.environ: + cmake_options.append("-DLIBFMT_VENDORIZE=OFF") + # The information here can also be placed in setup.cfg - better separation of # logic and declaration, and simpler if you include description/version in a file. -setuptools.setup( +skbuild.setup( name="simplepyble", version=version_str, author="Kevin Dewald", @@ -45,34 +63,27 @@ url="https://github.com/OpenBluetoothToolbox/SimpleBLE", description="The ultimate fully-fledged cross-platform BLE library, designed for simplicity and ease of use.", long_description=long_description, - long_description_content_type='text/x-rst', - ext_modules=[ - cmake_build_extension.CMakeExtension( - name="simplepyble", - disable_editable=True, - source_dir=here, - cmake_depends_on=["pybind11"], - cmake_configure_options=cmake_options, - cmake_generator=None, - ) - ], - cmdclass={ - "build_ext": cmake_build_extension.BuildExtension - }, - zip_safe=False, - install_requires=[ - "wheel", + long_description_content_type="text/x-rst", + packages=["simplepyble"], + package_dir={"": "simplepyble/src"}, + cmake_source_dir="simplepyble", + cmake_args=cmake_options, + cmake_process_manifest_hook=exclude_unnecessary_files, + cmake_install_dir="simplepyble/src/simplepyble", + setup_requires=[ + "setuptools>=42", + "scikit-build", + "ninja; platform_system!='Windows'", + "cmake>=3.21", "pybind11", - "ninja" - ], - test_requires=[ - "pytest", ], + install_requires=[], extras_require={}, platforms="Windows, macOS, Linux", python_requires=">=3.7", classifiers=[ - "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "License :: Other/Proprietary License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", diff --git a/third_party/SimpleBLE/simpleble/CMakeLists.txt b/third_party/SimpleBLE/simpleble/CMakeLists.txt index ad8a70fca..3f205d313 100644 --- a/third_party/SimpleBLE/simpleble/CMakeLists.txt +++ b/third_party/SimpleBLE/simpleble/CMakeLists.txt @@ -2,25 +2,30 @@ cmake_minimum_required(VERSION 3.21) include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/prelude.cmake) -project(SimpleBLE VERSION 0.1 LANGUAGES CXX) +project( + simpleble + VERSION ${SIMPLEBLE_VERSION} + DESCRIPTION "The ultimate fully-fledged cross-platform library for Bluetooth Low Energy (BLE)." + HOMEPAGE_URL "https://github.com/OpenBluetoothToolbox/SimpleBLE" + LANGUAGES CXX +) -if(APPLE) +if (APPLE) SET(SIMPLEBLE-C simpleble-c) SET(FILE_NAME "libsimpleble-c.dylib") -elseif(UNIX) +elseif (UNIX) SET(SIMPLEBLE-C simpleble-c) SET(FILE_NAME "libsimpleble-c.so") -else() - if(CMAKE_SIZEOF_VOID_P EQUAL 8) +else () + if(CMAKE_SIZEOF_VOID_P EQUAL 8) SET(SIMPLEBLE-C simpleble-c) SET(FILE_NAME "simpleble-c.dll") else(CMAKE_SIZEOF_VOID_P EQUAL 8) SET(SIMPLEBLE-C simpleble-c32) SET(FILE_NAME "simpleble-c32.dll") endif(CMAKE_SIZEOF_VOID_P EQUAL 8) -endif(APPLE) +endif (APPLE) -# Run prelude script to set up environment include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/epilogue.cmake) include(GenerateExportHeader) @@ -29,6 +34,14 @@ include(GNUInstallDirs) option(SIMPLEBLE_PLAIN "Use plain version of SimpleBLE" OFF) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../fmt ${CMAKE_CURRENT_BINARY_DIR}/libfmt) +#if(NOT TARGET fmt::fmt-header-only) +# option(LIBFMT_VENDORIZE "Enable vendorized libfmt" ON) +# find_package(fmt REQUIRED) + +# if(TARGET fmt) +# set_target_properties(fmt PROPERTIES EXCLUDE_FROM_ALL TRUE) +# endif() +#endif() if(SIMPLEBLE_TEST) message(STATUS "Building tests requires plain version of SimpleBLE") @@ -41,7 +54,8 @@ set(SIMPLEBLE_PRIVATE_INCLUDES ${CMAKE_CURRENT_SOURCE_DIR}/src/builders ${CMAKE_CURRENT_SOURCE_DIR}/src/external ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/common - ${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/safe) + ${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/safe + ${CMAKE_CURRENT_SOURCE_DIR}/../external/include) set(SIMPLEBLE_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/base/Adapter.cpp @@ -71,12 +85,16 @@ set(SIMPLEBLE_C_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src_c/simpleble.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src_c/adapter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src_c/peripheral.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src_c/utils.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src_c/logging.cpp) + ${CMAKE_CURRENT_SOURCE_DIR}/src_c/logging.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src_c/utils.cpp) # Define targets add_library(simpleble ${SIMPLEBLE_SRC}) -add_library(${SIMPLEBLE-C} SHARED ${SIMPLEBLE_C_SRC}) +# BrainFlow: convert to shared library +add_library(${SIMPLEBLE-C} SHARED ${SIMPLEBLE_C_SRC}) + +add_library(simpleble::simpleble ALIAS simpleble) +add_library(simpleble::simpleble-c ALIAS ${SIMPLEBLE-C}) set_target_properties(simpleble PROPERTIES CXX_VISIBILITY_PRESET hidden @@ -88,7 +106,11 @@ set_target_properties(simpleble PROPERTIES VERSION "${PROJECT_VERSION}" SOVERSION "${PROJECT_VERSION_MAJOR}" EXPORT_NAME simpleble - OUTPUT_NAME simpleble) + OUTPUT_NAME simpleble + RELEASE_POSTFIX "" + RELWITHDEBINFO_POSTFIX "-relwithdebinfo" + MINSIZEREL_POSTFIX "-minsizerel" + DEBUG_POSTFIX "-debug") set_target_properties(${SIMPLEBLE-C} PROPERTIES C_VISIBILITY_PRESET hidden @@ -100,13 +122,22 @@ set_target_properties(${SIMPLEBLE-C} PROPERTIES DEFINE_SYMBOL simpleble_EXPORTS # Use the same symbol as simpleble ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../../../compiled LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../../../compiled - RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../../../compiled) - -generate_export_header( - simpleble - BASE_NAME simpleble - EXPORT_FILE_NAME export/simpleble/export.h -) + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../../../compiled + VERSION "${PROJECT_VERSION}" + SOVERSION "${PROJECT_VERSION_MAJOR}" + EXPORT_NAME ${SIMPLEBLE-C} + OUTPUT_NAME ${SIMPLEBLE-C} + RELEASE_POSTFIX "" + RELWITHDEBINFO_POSTFIX "-relwithdebinfo" + MINSIZEREL_POSTFIX "-minsizerel" + DEBUG_POSTFIX "-debug") + +# BrainFlow, no need for header +#generate_export_header( +# simpleble +# BASE_NAME simpleble +# EXPORT_FILE_NAME export/simpleble/export.h +#) # Configure include directories target_include_directories(simpleble PRIVATE ${SIMPLEBLE_PRIVATE_INCLUDES}) @@ -114,6 +145,7 @@ target_include_directories(${SIMPLEBLE-C} PRIVATE ${SIMPLEBLE_PRIVATE_INCLUDES}) target_include_directories(simpleble INTERFACE $ + $ $) target_include_directories(${SIMPLEBLE-C} INTERFACE $ @@ -128,14 +160,16 @@ target_include_directories(${SIMPLEBLE-C} SYSTEM PUBLIC # Configure linked libraries target_link_libraries(simpleble PRIVATE $) target_link_libraries(${SIMPLEBLE-C} PRIVATE $) -target_link_libraries(${SIMPLEBLE-C} PRIVATE simpleble) +target_link_libraries(${SIMPLEBLE-C} PRIVATE simpleble::simpleble) + +append_sanitize_options("${SIMPLEBLE_SANITIZE}") if(NOT SIMPLEBLE_LOG_LEVEL) -set(SIMPLEBLE_LOG_LEVEL "INFO") + set(SIMPLEBLE_LOG_LEVEL "INFO") endif() -list(APPEND PRIVATE_COMPILE_DEFINITIONS SIMPLEBLE_LOG_LEVEL=SIMPLEBLE_LOG_LEVEL_${SIMPLEBLE_LOG_LEVEL}) -append_sanitize_options("${SIMPLEBLE_SANITIZE}") +list(APPEND PRIVATE_COMPILE_DEFINITIONS SIMPLEBLE_LOG_LEVEL=SIMPLEBLE_LOG_LEVEL_${SIMPLEBLE_LOG_LEVEL}) +list(APPEND PRIVATE_COMPILE_DEFINITIONS SIMPLEBLE_VERSION="${PROJECT_VERSION}") # Detect the operating system and load the necessary dependencies if(SIMPLEBLE_PLAIN) @@ -164,6 +198,10 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") list(APPEND PRIVATE_COMPILE_DEFINITIONS SIMPLEDBUS_LOG_LEVEL=${SIMPLEDBUS_LOG_LEVEL}) list(APPEND PRIVATE_COMPILE_DEFINITIONS SIMPLEBLUEZ_LOG_LEVEL=${SIMPLEBLUEZ_LOG_LEVEL}) + if(SIMPLEBLE_USE_SESSION_DBUS) + list(APPEND PRIVATE_COMPILE_DEFINITIONS SIMPLEBLUEZ_USE_SESSION_DBUS) + endif() + target_sources(simpleble PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/linux/AdapterBase.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/linux/PeripheralBase.cpp @@ -211,6 +249,7 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") ${CMAKE_CURRENT_SOURCE_DIR}/../simpledbus/include ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/linux) + set_property(TARGET simpleble PROPERTY INSTALL_RPATH $ORIGIN) set_property(TARGET ${SIMPLEBLE-C} PROPERTY INSTALL_RPATH $ORIGIN) elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") @@ -226,8 +265,12 @@ elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") # /D_USE_MATH_DEFINES -> Specifies that the math.h header file should be included. list(APPEND PRIVATE_COMPILE_DEFINITIONS "/D_USE_MATH_DEFINES") + # /utf-8 -> Set source and executable character sets to utf-8. https://learn.microsoft.com/en-us/cpp/build/reference/utf-8-set-source-and-executable-character-sets-to-utf-8 + list(APPEND PRIVATE_COMPILE_OPTIONS "/utf-8") # /Gd -> Use __cdecl as the default calling convention. https://docs.microsoft.com/en-us/cpp/cpp/cdecl list(APPEND PRIVATE_COMPILE_OPTIONS "/Gd") + # /EHsc -> Use the standard C++ exception handling model. https://learn.microsoft.com/en-us/cpp/build/reference/eh-exception-handling-model + list(APPEND PRIVATE_COMPILE_OPTIONS "/EHsc") # /WX -> Treats all warnings as errors. list(APPEND PRIVATE_COMPILE_OPTIONS "/WX") # /W1 -> Use the lowest level of warnings, as there are some unsafe functions that MSVC doesn't like. @@ -235,7 +278,8 @@ elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") list(APPEND PRIVATE_COMPILE_OPTIONS "/W1") endif() - target_include_directories(simpleble PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/windows) + target_include_directories(simpleble PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/windows) target_sources(simpleble PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/windows/AdapterBase.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/windows/PeripheralBase.cpp @@ -247,7 +291,8 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR CMAKE_SYSTEM_NAME STREQUAL "iOS") list(APPEND PRIVATE_COMPILE_OPTIONS -fobjc-arc) target_link_libraries(simpleble PUBLIC "-framework Foundation" "-framework CoreBluetooth" ObjC) - target_include_directories(simpleble PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/macos) + target_include_directories(simpleble PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/macos) target_sources(simpleble PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/macos/Utils.mm ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/macos/AdapterBase.mm @@ -255,10 +300,44 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR CMAKE_SYSTEM_NAME STREQUAL "iOS") ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/macos/PeripheralBase.mm ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/macos/PeripheralBaseMacOS.mm) + set_property(TARGET simpleble PROPERTY INSTALL_RPATH @loader_path) set_property(TARGET ${SIMPLEBLE-C} PROPERTY INSTALL_RPATH @loader_path) -else() - message(FATAL_ERROR "-- [ERROR] UNSUPPORTED SYSTEM: ${CMAKE_HOST_SYSTEM_NAME} ${CMAKE_SYSTEM_NAME}") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Android") + message(STATUS "Configuring for Android") + + # Set the include directories for the Android NDK headers and other necessary directories + include_directories(${ANDROID_NDK}/sysroot/usr/include) + include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/backends/android) + + # Add any necessary compile definitions or options for Android + add_compile_options(-DANDROID -D__ANDROID_API__=${ANDROID_NATIVE_API_LEVEL}) + + # Specify the source files for the Android backend + # NOTE: These files have been commented out and replaced by the PLAIN version in order to develop the Android wrapper code. + target_include_directories(simpleble PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/android) + target_sources(simpleble PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/android/AdapterBase.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/android/PeripheralBase.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/android/android/BluetoothDevice.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/android/android/BluetoothGatt.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/android/android/BluetoothGattService.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/android/android/BluetoothGattCharacteristic.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/android/android/BluetoothGattDescriptor.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/android/android/UUID.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/android/android/ScanResult.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/android/bridge/BluetoothGattCallback.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/android/bridge/ScanCallback.cpp + ) + + target_include_directories(simpleble PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/backends/plain) + + target_link_libraries(simpleble PUBLIC + android + nativehelper + log) endif() apply_build_options(simpleble @@ -273,6 +352,42 @@ apply_build_options(${SIMPLEBLE-C} "${PRIVATE_LINK_OPTIONS}" "${PUBLIC_LINK_OPTIONS}") +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/simpleble.pc.in + ${CMAKE_CURRENT_BINARY_DIR}/simpleble.pc @ONLY) + +# BrainFlow does not install simpleble +#install( +# TARGETS simpleble ${SIMPLEBLE-C} +# EXPORT simpleble-config +# ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} +# LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +# RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}) # BrainFlow output to libdir + +#install( +# EXPORT simpleble-config +# NAMESPACE simpleble:: +# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/simpleble) + +#install( +# DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/include/simpleble/ +# DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/simpleble) + +#install( +# DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/../external/include/ +# DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/simpleble) + +#install( +# DIRECTORY ${PROJECT_BINARY_DIR}/export/simpleble/ +# DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/simpleble) + +#install( +# DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/include/simpleble_c/ +# DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/simpleble_c) + +#install( +# FILES ${CMAKE_CURRENT_BINARY_DIR}/simpleble.pc +# DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) + if(SIMPLEBLE_TEST) message(STATUS "Building Tests") find_package(GTest REQUIRED) @@ -280,6 +395,7 @@ if(SIMPLEBLE_TEST) add_executable(simpleble_test ${CMAKE_CURRENT_SOURCE_DIR}/test/src/main.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test/src/test_utils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test/src/test_bytearray.cpp ) set_target_properties(simpleble_test PROPERTIES @@ -289,11 +405,12 @@ if(SIMPLEBLE_TEST) POSITION_INDEPENDENT_CODE ON WINDOWS_EXPORT_ALL_SYMBOLS ON) - target_link_libraries(simpleble_test PRIVATE simpleble GTest::gtest) + target_link_libraries(simpleble_test PRIVATE simpleble::simpleble GTest::gtest) endif() + if (MSVC) - add_custom_command (TARGET ${SIMPLEBLE-C} POST_BUILD + add_custom_command(TARGET ${SIMPLEBLE-C} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../../../compiled/$/${FILE_NAME}" "${CMAKE_CURRENT_SOURCE_DIR}/../../../python_package/brainflow/lib/${FILE_NAME}" COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../../../compiled/$/${FILE_NAME}" "${CMAKE_CURRENT_SOURCE_DIR}/../../../java_package/brainflow/src/main/resources/${FILE_NAME}" COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../../../compiled/$/${FILE_NAME}" "${CMAKE_CURRENT_SOURCE_DIR}/../../../csharp_package/brainflow/brainflow/lib/${FILE_NAME}" @@ -304,7 +421,7 @@ if (MSVC) ) endif (MSVC) if (UNIX AND NOT ANDROID) - add_custom_command (TARGET ${SIMPLEBLE-C} POST_BUILD + add_custom_command(TARGET ${SIMPLEBLE-C} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../../../compiled/${FILE_NAME}" "${CMAKE_CURRENT_SOURCE_DIR}/../../../python_package/brainflow/lib/${FILE_NAME}" COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../../../compiled/${FILE_NAME}" "${CMAKE_CURRENT_SOURCE_DIR}/../../../julia_package/brainflow/lib/${FILE_NAME}" COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/../../../compiled/${FILE_NAME}" "${CMAKE_CURRENT_SOURCE_DIR}/../../../java_package/brainflow/src/main/resources/${FILE_NAME}" @@ -316,16 +433,16 @@ if (UNIX AND NOT ANDROID) endif (UNIX AND NOT ANDROID) if (MSVC) - install ( + install( FILES ${CMAKE_CURRENT_SOURCE_DIR}/../../../compiled/$/${FILE_NAME} DESTINATION lib ) endif (MSVC) if (UNIX AND NOT ANDROID) - install ( + install( FILES ${CMAKE_CURRENT_SOURCE_DIR}/../../../compiled/${FILE_NAME} DESTINATION lib ) -endif (UNIX AND NOT ANDROID) +endif (UNIX AND NOT ANDROID) \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/include/simpleble/Adapter.h b/third_party/SimpleBLE/simpleble/include/simpleble/Adapter.h index 9575e01af..f8ddfe8da 100644 --- a/third_party/SimpleBLE/simpleble/include/simpleble/Adapter.h +++ b/third_party/SimpleBLE/simpleble/include/simpleble/Adapter.h @@ -5,8 +5,6 @@ #include #include -#include - #include #include #include @@ -15,7 +13,7 @@ namespace SimpleBLE { class AdapterBase; -class SIMPLEBLE_EXPORT Adapter { +class Adapter { public: Adapter() = default; virtual ~Adapter() = default; diff --git a/third_party/SimpleBLE/simpleble/include/simpleble/AdapterSafe.h b/third_party/SimpleBLE/simpleble/include/simpleble/AdapterSafe.h index 00a2a59ce..8e766e5bf 100644 --- a/third_party/SimpleBLE/simpleble/include/simpleble/AdapterSafe.h +++ b/third_party/SimpleBLE/simpleble/include/simpleble/AdapterSafe.h @@ -1,7 +1,5 @@ #pragma once -#include - #include #include @@ -9,7 +7,7 @@ namespace SimpleBLE { namespace Safe { -class SIMPLEBLE_EXPORT Adapter : public SimpleBLE::Adapter { +class Adapter : public SimpleBLE::Adapter { public: Adapter(SimpleBLE::Adapter& adapter); virtual ~Adapter() = default; diff --git a/third_party/SimpleBLE/simpleble/include/simpleble/Characteristic.h b/third_party/SimpleBLE/simpleble/include/simpleble/Characteristic.h index e9692322e..da60b9812 100644 --- a/third_party/SimpleBLE/simpleble/include/simpleble/Characteristic.h +++ b/third_party/SimpleBLE/simpleble/include/simpleble/Characteristic.h @@ -2,8 +2,6 @@ #include -#include - #include #include #include @@ -12,7 +10,7 @@ namespace SimpleBLE { class CharacteristicBase; -class SIMPLEBLE_EXPORT Characteristic { +class Characteristic { public: Characteristic() = default; virtual ~Characteristic() = default; diff --git a/third_party/SimpleBLE/simpleble/include/simpleble/Descriptor.h b/third_party/SimpleBLE/simpleble/include/simpleble/Descriptor.h index 5fd1cff20..65e0f8a7d 100644 --- a/third_party/SimpleBLE/simpleble/include/simpleble/Descriptor.h +++ b/third_party/SimpleBLE/simpleble/include/simpleble/Descriptor.h @@ -2,8 +2,6 @@ #include -#include - #include #include @@ -11,7 +9,7 @@ namespace SimpleBLE { class DescriptorBase; -class SIMPLEBLE_EXPORT Descriptor { +class Descriptor { public: Descriptor() = default; virtual ~Descriptor() = default; diff --git a/third_party/SimpleBLE/simpleble/include/simpleble/Exceptions.h b/third_party/SimpleBLE/simpleble/include/simpleble/Exceptions.h index b20d6047f..6ddfc3550 100644 --- a/third_party/SimpleBLE/simpleble/include/simpleble/Exceptions.h +++ b/third_party/SimpleBLE/simpleble/include/simpleble/Exceptions.h @@ -3,66 +3,64 @@ #include #include -#include - #include "Types.h" namespace SimpleBLE { namespace Exception { -class SIMPLEBLE_EXPORT BaseException : public std::runtime_error { +class BaseException : public std::runtime_error { public: BaseException(const std::string& __arg) : std::runtime_error(__arg) {} }; -class SIMPLEBLE_EXPORT NotInitialized : public BaseException { +class NotInitialized : public BaseException { public: NotInitialized(); }; -class SIMPLEBLE_EXPORT NotConnected : public BaseException { +class NotConnected : public BaseException { public: NotConnected(); }; -class SIMPLEBLE_EXPORT InvalidReference : public BaseException { +class InvalidReference : public BaseException { public: InvalidReference(); }; -class SIMPLEBLE_EXPORT ServiceNotFound : public BaseException { +class ServiceNotFound : public BaseException { public: ServiceNotFound(BluetoothUUID uuid); }; -class SIMPLEBLE_EXPORT CharacteristicNotFound : public BaseException { +class CharacteristicNotFound : public BaseException { public: CharacteristicNotFound(BluetoothUUID uuid); }; -class SIMPLEBLE_EXPORT DescriptorNotFound : public BaseException { +class DescriptorNotFound : public BaseException { public: DescriptorNotFound(BluetoothUUID uuid); }; -class SIMPLEBLE_EXPORT OperationNotSupported : public BaseException { +class OperationNotSupported : public BaseException { public: OperationNotSupported(); }; -class SIMPLEBLE_EXPORT OperationFailed : public BaseException { +class OperationFailed : public BaseException { public: OperationFailed(); OperationFailed(const std::string& err_msg); }; -class SIMPLEBLE_EXPORT WinRTException : public BaseException { +class WinRTException : public BaseException { public: WinRTException(int32_t err_code, const std::string& err_msg); }; -class SIMPLEBLE_EXPORT CoreBluetoothException : public BaseException { +class CoreBluetoothException : public BaseException { public: CoreBluetoothException(const std::string& err_msg); }; diff --git a/third_party/SimpleBLE/simpleble/include/simpleble/Logging.h b/third_party/SimpleBLE/simpleble/include/simpleble/Logging.h index 84a3f7c93..e517657fe 100644 --- a/third_party/SimpleBLE/simpleble/include/simpleble/Logging.h +++ b/third_party/SimpleBLE/simpleble/include/simpleble/Logging.h @@ -5,8 +5,6 @@ #include #include -#include - namespace SimpleBLE { namespace Logging { @@ -25,7 +23,7 @@ enum Level : int { using Callback = std::function; // clang-format on -class SIMPLEBLE_EXPORT Logger { +class Logger { public: static Logger* get(); diff --git a/third_party/SimpleBLE/simpleble/include/simpleble/Peripheral.h b/third_party/SimpleBLE/simpleble/include/simpleble/Peripheral.h index 594627aaa..3917c997b 100644 --- a/third_party/SimpleBLE/simpleble/include/simpleble/Peripheral.h +++ b/third_party/SimpleBLE/simpleble/include/simpleble/Peripheral.h @@ -7,8 +7,6 @@ #include #include -#include - #include #include #include @@ -17,7 +15,7 @@ namespace SimpleBLE { class PeripheralBase; -class SIMPLEBLE_EXPORT Peripheral { +class Peripheral { public: Peripheral() = default; virtual ~Peripheral() = default; diff --git a/third_party/SimpleBLE/simpleble/include/simpleble/PeripheralSafe.h b/third_party/SimpleBLE/simpleble/include/simpleble/PeripheralSafe.h index 1a6f86e9e..6d40ced85 100644 --- a/third_party/SimpleBLE/simpleble/include/simpleble/PeripheralSafe.h +++ b/third_party/SimpleBLE/simpleble/include/simpleble/PeripheralSafe.h @@ -2,8 +2,6 @@ #include -#include - #include #include @@ -11,7 +9,7 @@ namespace SimpleBLE { namespace Safe { -class SIMPLEBLE_EXPORT Peripheral : public SimpleBLE::Peripheral { +class Peripheral : public SimpleBLE::Peripheral { public: Peripheral(SimpleBLE::Peripheral& peripheral); virtual ~Peripheral() = default; diff --git a/third_party/SimpleBLE/simpleble/include/simpleble/Service.h b/third_party/SimpleBLE/simpleble/include/simpleble/Service.h index 32f0230e9..09bcea04a 100644 --- a/third_party/SimpleBLE/simpleble/include/simpleble/Service.h +++ b/third_party/SimpleBLE/simpleble/include/simpleble/Service.h @@ -3,8 +3,6 @@ #include #include -#include - #include #include #include "simpleble/Characteristic.h" @@ -13,7 +11,7 @@ namespace SimpleBLE { class ServiceBase; -class SIMPLEBLE_EXPORT Service { +class Service { public: Service() = default; virtual ~Service() = default; diff --git a/third_party/SimpleBLE/simpleble/include/simpleble/Types.h b/third_party/SimpleBLE/simpleble/include/simpleble/Types.h index fe67c08b9..e68efdb92 100644 --- a/third_party/SimpleBLE/simpleble/include/simpleble/Types.h +++ b/third_party/SimpleBLE/simpleble/include/simpleble/Types.h @@ -3,6 +3,12 @@ #include #include #include +#include "external/kvn_bytearray.h" + +/** + * @file Types.h + * @brief Defines types and enumerations used throughout the SimpleBLE library. + */ namespace SimpleBLE { @@ -12,9 +18,11 @@ using BluetoothAddress = std::string; // returns the same string, but provides a homogeneous interface. using BluetoothUUID = std::string; -// IDEA: Extend ByteArray to be constructed by a vector of bytes -// and pointers to uint8_t. -using ByteArray = std::string; +/** + * @typedef ByteArray + * @brief Represents a byte array using kvn::bytearray from the external library. + */ +using ByteArray = kvn::bytearray; enum class OperatingSystem { WINDOWS, @@ -22,6 +30,7 @@ enum class OperatingSystem { LINUX, }; +// TODO: Add to_string functions for all enums. enum BluetoothAddressType : int32_t { PUBLIC = 0, RANDOM = 1, UNSPECIFIED = 2 }; } // namespace SimpleBLE diff --git a/third_party/SimpleBLE/simpleble/include/simpleble/Utils.h b/third_party/SimpleBLE/simpleble/include/simpleble/Utils.h index f270b7f8e..889e6fad7 100644 --- a/third_party/SimpleBLE/simpleble/include/simpleble/Utils.h +++ b/third_party/SimpleBLE/simpleble/include/simpleble/Utils.h @@ -1,13 +1,11 @@ #pragma once -#include - #include namespace SimpleBLE { -OperatingSystem SIMPLEBLE_EXPORT get_operating_system(); +OperatingSystem get_operating_system(); -std::string SIMPLEBLE_EXPORT get_simpleble_version(); +std::string get_simpleble_version(); } // namespace SimpleBLE diff --git a/third_party/SimpleBLE/simpleble/include/simpleble_c/adapter.h b/third_party/SimpleBLE/simpleble/include/simpleble_c/adapter.h index a479c74c1..c47bea816 100644 --- a/third_party/SimpleBLE/simpleble/include/simpleble_c/adapter.h +++ b/third_party/SimpleBLE/simpleble/include/simpleble_c/adapter.h @@ -4,8 +4,6 @@ #include #include -#include - #include #ifdef _WIN32 diff --git a/third_party/SimpleBLE/simpleble/include/simpleble_c/logging.h b/third_party/SimpleBLE/simpleble/include/simpleble_c/logging.h index 0cee8ab00..de2b57bb5 100644 --- a/third_party/SimpleBLE/simpleble/include/simpleble_c/logging.h +++ b/third_party/SimpleBLE/simpleble/include/simpleble_c/logging.h @@ -2,8 +2,6 @@ #include -#include - #ifdef _WIN32 #define SHARED_EXPORT __declspec(dllexport) #define CALLING_CONVENTION __cdecl diff --git a/third_party/SimpleBLE/simpleble/include/simpleble_c/peripheral.h b/third_party/SimpleBLE/simpleble/include/simpleble_c/peripheral.h index e20c3574f..8372681d3 100644 --- a/third_party/SimpleBLE/simpleble/include/simpleble_c/peripheral.h +++ b/third_party/SimpleBLE/simpleble/include/simpleble_c/peripheral.h @@ -4,8 +4,6 @@ #include #include -#include - #include #ifdef _WIN32 @@ -228,11 +226,11 @@ SHARED_EXPORT simpleble_err_t CALLING_CONVENTION simpleble_peripheral_write_comm * @param callback * @return simpleble_err_t */ -SHARED_EXPORT simpleble_err_t CALLING_CONVENTION -simpleble_peripheral_notify(simpleble_peripheral_t handle, simpleble_uuid_t service, simpleble_uuid_t characteristic, - void (*callback)(simpleble_uuid_t service, simpleble_uuid_t characteristic, - const uint8_t* data, size_t data_length, void* userdata), - void* userdata); +SHARED_EXPORT simpleble_err_t CALLING_CONVENTION simpleble_peripheral_notify( + simpleble_peripheral_t handle, simpleble_uuid_t service, simpleble_uuid_t characteristic, + void (*callback)(simpleble_peripheral_t handle, simpleble_uuid_t service, simpleble_uuid_t characteristic, + const uint8_t* data, size_t data_length, void* userdata), + void* userdata); /** * @brief @@ -243,11 +241,11 @@ simpleble_peripheral_notify(simpleble_peripheral_t handle, simpleble_uuid_t serv * @param callback * @return simpleble_err_t */ -SHARED_EXPORT simpleble_err_t CALLING_CONVENTION -simpleble_peripheral_indicate(simpleble_peripheral_t handle, simpleble_uuid_t service, simpleble_uuid_t characteristic, - void (*callback)(simpleble_uuid_t service, simpleble_uuid_t characteristic, - const uint8_t* data, size_t data_length, void* userdata), - void* userdata); +SHARED_EXPORT simpleble_err_t CALLING_CONVENTION simpleble_peripheral_indicate( + simpleble_peripheral_t handle, simpleble_uuid_t service, simpleble_uuid_t characteristic, + void (*callback)(simpleble_peripheral_t handle, simpleble_uuid_t service, simpleble_uuid_t characteristic, + const uint8_t* data, size_t data_length, void* userdata), + void* userdata); /** * @brief diff --git a/third_party/SimpleBLE/simpleble/include/simpleble_c/simpleble.h b/third_party/SimpleBLE/simpleble/include/simpleble_c/simpleble.h index 626e4263a..8f3692f4d 100644 --- a/third_party/SimpleBLE/simpleble/include/simpleble_c/simpleble.h +++ b/third_party/SimpleBLE/simpleble/include/simpleble_c/simpleble.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include diff --git a/third_party/SimpleBLE/simpleble/include/simpleble_c/utils.h b/third_party/SimpleBLE/simpleble/include/simpleble_c/utils.h index e72864e2a..ea598a5d7 100644 --- a/third_party/SimpleBLE/simpleble/include/simpleble_c/utils.h +++ b/third_party/SimpleBLE/simpleble/include/simpleble_c/utils.h @@ -1,7 +1,5 @@ #pragma once -#include - #include #ifdef _WIN32 diff --git a/third_party/SimpleBLE/simpleble/src/Utils.cpp b/third_party/SimpleBLE/simpleble/src/Utils.cpp index c6a45319c..45e2a5b23 100644 --- a/third_party/SimpleBLE/simpleble/src/Utils.cpp +++ b/third_party/SimpleBLE/simpleble/src/Utils.cpp @@ -12,6 +12,6 @@ OperatingSystem get_operating_system() { #endif } -std::string get_simpleble_version() { return "0.6.1"; } +std::string get_simpleble_version() { return SIMPLEBLE_VERSION; } } // namespace SimpleBLE \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/AdapterBase.cpp b/third_party/SimpleBLE/simpleble/src/backends/android/AdapterBase.cpp new file mode 100644 index 000000000..4b2da7414 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/AdapterBase.cpp @@ -0,0 +1,167 @@ +#include "AdapterBase.h" +#include "CommonUtils.h" +#include "PeripheralBase.h" +#include "PeripheralBuilder.h" + +#include +#include +#include +#include +#include +#include + +using namespace SimpleBLE; + +JNI::Class AdapterBase::_btAdapterCls; +JNI::Class AdapterBase::_btScanResultCls; + +JNI::Object AdapterBase::_btAdapter; +JNI::Object AdapterBase::_btScanner; + +void AdapterBase::initialize() { + JNI::Env env; + + // Check if the BluetoothAdapter class has been loaded + if (_btAdapterCls.get() == nullptr) { + _btAdapterCls = env.find_class("android/bluetooth/BluetoothAdapter"); + } + + if (_btScanResultCls.get() == nullptr) { + _btScanResultCls = env.find_class("android/bluetooth/le/ScanResult"); + } + + if (_btAdapter.get() == nullptr) { + _btAdapter = _btAdapterCls.call_static_method( "getDefaultAdapter", "()Landroid/bluetooth/BluetoothAdapter;"); + } + + if (_btScanner.get() == nullptr) { + _btScanner = _btAdapter.call_object_method("getBluetoothLeScanner", "()Landroid/bluetooth/le/BluetoothLeScanner;"); + } + +} + +std::vector> AdapterBase::get_adapters() { + initialize(); + + // Create an instance of AdapterBase and add it to the vector + std::shared_ptr adapter = std::make_shared(); + std::vector> adapters; + adapters.push_back(adapter); + + return adapters; +} + +bool AdapterBase::bluetooth_enabled() { + initialize(); + + bool isEnabled = _btAdapter.call_boolean_method("isEnabled", "()Z"); + int bluetoothState = _btAdapter.call_int_method("getState", "()I"); + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", fmt::format("Bluetooth state: {}", bluetoothState).c_str()); + + return isEnabled; //bluetoothState == 12; +} + +AdapterBase::AdapterBase() { + _btScanCallback.set_callback_onScanResult([this](Android::ScanResult scan_result) { + std::string address = scan_result.getDevice().getAddress(); + + if (this->peripherals_.count(address) == 0) { + // If the incoming peripheral has never been seen before, create and save a reference to it. + auto base_peripheral = std::make_shared(scan_result); + this->peripherals_.insert(std::make_pair(address, base_peripheral)); + } + + // Update the received advertising data. + auto base_peripheral = this->peripherals_.at(address); + base_peripheral->update_advertising_data(scan_result); + + // Convert the base object into an external-facing Peripheral object + PeripheralBuilder peripheral_builder(base_peripheral); + + // Check if the device has been seen before, to forward the correct call to the user. + if (this->seen_peripherals_.count(address) == 0) { + // Store it in our table of seen peripherals + this->seen_peripherals_.insert(std::make_pair(address, base_peripheral)); + SAFE_CALLBACK_CALL(this->callback_on_scan_found_, peripheral_builder); + } else { + SAFE_CALLBACK_CALL(this->callback_on_scan_updated_, peripheral_builder); + } + }); + +} + +AdapterBase::~AdapterBase() { + +} + +void* AdapterBase::underlying() const { return nullptr; } + +std::string AdapterBase::identifier() { + return _btAdapter.call_string_method("getName", "()Ljava/lang/String;"); +} + +BluetoothAddress AdapterBase::address() { + return BluetoothAddress(_btAdapter.call_string_method("getAddress", "()Ljava/lang/String;")); +} + +void AdapterBase::scan_start() { + seen_peripherals_.clear(); + _btScanner.call_void_method("startScan", "(Landroid/bluetooth/le/ScanCallback;)V", _btScanCallback.get()); + scanning_ = true; + SAFE_CALLBACK_CALL(this->callback_on_scan_start_); +} + +void AdapterBase::scan_stop() { + _btScanner.call_void_method("stopScan", "(Landroid/bluetooth/le/ScanCallback;)V", _btScanCallback.get()); + scanning_ = false; + SAFE_CALLBACK_CALL(this->callback_on_scan_stop_); +} + +void AdapterBase::scan_for(int timeout_ms) { + scan_start(); + std::this_thread::sleep_for(std::chrono::milliseconds(timeout_ms)); + scan_stop(); +} + +bool AdapterBase::scan_is_active() { return scanning_; } + +std::vector AdapterBase::scan_get_results() { + return std::vector(); +} + +std::vector AdapterBase::get_paired_peripherals() { + return std::vector(); +} + +void AdapterBase::set_callback_on_scan_start(std::function on_scan_start) { + if (on_scan_start) { + callback_on_scan_start_.load(on_scan_start); + } else { + callback_on_scan_start_.unload(); + } +} + +void AdapterBase::set_callback_on_scan_stop(std::function on_scan_stop) { + if (on_scan_stop) { + callback_on_scan_stop_.load(on_scan_stop); + } else { + callback_on_scan_stop_.unload(); + } +} + +void AdapterBase::set_callback_on_scan_updated(std::function on_scan_updated) { + if (on_scan_updated) { + callback_on_scan_updated_.load(on_scan_updated); + } else { + callback_on_scan_updated_.unload(); + } +} + +void AdapterBase::set_callback_on_scan_found(std::function on_scan_found) { + if (on_scan_found) { + callback_on_scan_found_.load(on_scan_found); + } else { + callback_on_scan_found_.unload(); + } +} + diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/AdapterBase.h b/third_party/SimpleBLE/simpleble/src/backends/android/AdapterBase.h new file mode 100644 index 000000000..10b9d1f94 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/AdapterBase.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include "jni/Common.hpp" +#include "bridge/ScanCallback.h" + +namespace SimpleBLE { + +class AdapterBase { + public: + AdapterBase(); + virtual ~AdapterBase(); + + void* underlying() const; + + std::string identifier(); + BluetoothAddress address(); + + void scan_start(); + void scan_stop(); + void scan_for(int timeout_ms); + bool scan_is_active(); + std::vector scan_get_results(); + + void set_callback_on_scan_start(std::function on_scan_start); + void set_callback_on_scan_stop(std::function on_scan_stop); + void set_callback_on_scan_updated(std::function on_scan_updated); + void set_callback_on_scan_found(std::function on_scan_found); + + std::vector get_paired_peripherals(); + + static bool bluetooth_enabled(); + static std::vector> get_adapters(); + + // NOTE: The following methods have been made public to allow the JNI layer to call them, but + // should not be called directly by the user. + + void onScanResultCallback(JNIEnv *env, jobject thiz, jint callback_type, jobject result); + void onBatchScanResultsCallback(JNIEnv *env, jobject thiz, jobject results); + void onScanFailedCallback(JNIEnv *env, jobject thiz, jint error_code); + + //static std::map _scanCallbackMap; + + private: + // NOTE: The correct way to request a BluetoothAdapter is to go though the BluetoothManager, + // as described in https://developer.android.com/reference/android/bluetooth/BluetoothManager#getAdapter() + // However, for simplicity, we are using a direct call to BluetoothAdapter.getDefaultAdapter() which is + // deprecated in API 31 but still works. We'll need to implement a backend bypass to get a Context + // object and call getSystemService(Context.BLUETOOTH_SERVICE) to get the BluetoothManager. + + void static initialize(); + // NOTE: Android BluetoothAdapter and BluetoothScanner classes are singletons, so we can use a static instance. + static JNI::Class _btAdapterCls; + static JNI::Class _btScanCallbackCls; + static JNI::Class _btScanResultCls; + static JNI::Object _btAdapter; + static JNI::Object _btScanner; + + Android::Bridge::ScanCallback _btScanCallback; + + std::map> peripherals_; + std::map> seen_peripherals_; + + kvn::safe_callback callback_on_scan_start_; + kvn::safe_callback callback_on_scan_stop_; + kvn::safe_callback callback_on_scan_updated_; + kvn::safe_callback callback_on_scan_found_; + + std::atomic scanning_{false}; + + +}; + +} // namespace SimpleBLE diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/PeripheralBase.cpp b/third_party/SimpleBLE/simpleble/src/backends/android/PeripheralBase.cpp new file mode 100644 index 000000000..48a8774e6 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/PeripheralBase.cpp @@ -0,0 +1,312 @@ +#include "PeripheralBase.h" + +#include "CharacteristicBuilder.h" +#include "DescriptorBuilder.h" +#include "ServiceBuilder.h" + +#include +#include +#include "CommonUtils.h" +#include "LoggingInternal.h" + +#include + +using namespace SimpleBLE; +using namespace std::chrono_literals; + +PeripheralBase::PeripheralBase(Android::ScanResult scan_result) : _device(scan_result.getDevice()) { + _btGattCallback.set_callback_onConnectionStateChange([this](bool connected) { + // If a connection has been established, request service discovery. + if (connected) { + _gatt.discoverServices(); + } else { + // TODO: Whatever cleanup is necessary when disconnected. + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", "Disconnected from device"); + SAFE_CALLBACK_CALL(callback_on_disconnected_); + } + }); + + _btGattCallback.set_callback_onServicesDiscovered([this]() { + _services = _gatt.getServices(); + + // Notify the user that the connection has been established once services have been discovered. + SAFE_CALLBACK_CALL(callback_on_connected_); + }); +} + +PeripheralBase::~PeripheralBase() {} + +void PeripheralBase::update_advertising_data(Android::ScanResult scan_result) {} + +void* PeripheralBase::underlying() const { return nullptr; } + +std::string PeripheralBase::identifier() { return _device.getName(); } + +BluetoothAddress PeripheralBase::address() { return BluetoothAddress(_device.getAddress()); } + +BluetoothAddressType PeripheralBase::address_type() { return BluetoothAddressType::UNSPECIFIED; } + +int16_t PeripheralBase::rssi() { return 0; } + +int16_t PeripheralBase::tx_power() { return 0; } + +uint16_t PeripheralBase::mtu() { return _btGattCallback.mtu; } + +void PeripheralBase::connect() { _gatt = _device.connectGatt(false, _btGattCallback); } + +void PeripheralBase::disconnect() { _gatt.disconnect(); } + +bool PeripheralBase::is_connected() { return _btGattCallback.connected && _btGattCallback.services_discovered; } + +bool PeripheralBase::is_connectable() { return false; } + +bool PeripheralBase::is_paired() { return false; } + +void PeripheralBase::unpair() {} + +std::vector PeripheralBase::services() { + std::vector service_list; + for (auto service : _services) { + // Build the list of characteristics for the service. + std::vector characteristic_list; + for (auto characteristic : service.getCharacteristics()) { + // Build the list of descriptors for the characteristic. + std::vector descriptor_list; + for (auto descriptor : characteristic.getDescriptors()) { + descriptor_list.push_back(DescriptorBuilder(descriptor.getUuid())); + } + + int flags = characteristic.getProperties(); + + bool can_read = flags & Android::BluetoothGattCharacteristic::PROPERTY_READ; + bool can_write_request = flags & Android::BluetoothGattCharacteristic::PROPERTY_WRITE; + bool can_write_command = flags & Android::BluetoothGattCharacteristic::PROPERTY_WRITE_NO_RESPONSE; + bool can_notify = flags & Android::BluetoothGattCharacteristic::PROPERTY_NOTIFY; + bool can_indicate = flags & Android::BluetoothGattCharacteristic::PROPERTY_INDICATE; + + characteristic_list.push_back(CharacteristicBuilder(characteristic.getUuid(), descriptor_list, can_read, + can_write_request, can_write_command, can_notify, + can_indicate)); + } + + service_list.push_back(ServiceBuilder(service.getUuid(), characteristic_list)); + } + + return service_list; +} + +std::vector PeripheralBase::advertised_services() { return std::vector(); } + +std::map PeripheralBase::manufacturer_data() { return std::map(); } + +ByteArray PeripheralBase::read(BluetoothUUID const& service, BluetoothUUID const& characteristic) { + auto msg = "Reading characteristic " + characteristic; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg.c_str()); + + auto characteristic_obj = _fetch_characteristic(service, characteristic); + + _btGattCallback.set_flag_characteristicReadPending(characteristic_obj.getObject()); + if (!_gatt.readCharacteristic(characteristic_obj)) { + throw SimpleBLE::Exception::OperationFailed("Failed to read characteristic " + characteristic); + } + + auto value = _btGattCallback.wait_flag_characteristicReadPending(characteristic_obj.getObject()); + return ByteArray(value.begin(), value.end()); +} + +void PeripheralBase::write_request(BluetoothUUID const& service, BluetoothUUID const& characteristic, + ByteArray const& data) { + auto msg = "Writing request to characteristic " + characteristic; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg.c_str()); + + auto characteristic_obj = _fetch_characteristic(service, characteristic); + + characteristic_obj.setWriteType(Android::BluetoothGattCharacteristic::WRITE_TYPE_DEFAULT); + characteristic_obj.setValue(std::vector(data.begin(), data.end())); + + _btGattCallback.set_flag_characteristicWritePending(characteristic_obj.getObject()); + if (!_gatt.writeCharacteristic(characteristic_obj)) { + throw SimpleBLE::Exception::OperationFailed("Failed to write characteristic " + characteristic); + } + _btGattCallback.wait_flag_characteristicWritePending(characteristic_obj.getObject()); +} + +void PeripheralBase::write_command(BluetoothUUID const& service, BluetoothUUID const& characteristic, + ByteArray const& data) { + auto msg = "Writing command to characteristic " + characteristic; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg.c_str()); + + auto characteristic_obj = _fetch_characteristic(service, characteristic); + + characteristic_obj.setWriteType(Android::BluetoothGattCharacteristic::WRITE_TYPE_NO_RESPONSE); + characteristic_obj.setValue(std::vector(data.begin(), data.end())); + + _btGattCallback.set_flag_characteristicWritePending(characteristic_obj.getObject()); + if (!_gatt.writeCharacteristic(characteristic_obj)) { + throw SimpleBLE::Exception::OperationFailed("Failed to write characteristic " + characteristic); + } + _btGattCallback.wait_flag_characteristicWritePending(characteristic_obj.getObject()); +} + +void PeripheralBase::notify(BluetoothUUID const& service, BluetoothUUID const& characteristic, + std::function callback) { + auto msg = "Subscribing to characteristic " + characteristic; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg.c_str()); + + auto characteristic_obj = _fetch_characteristic(service, characteristic); + auto descriptor_obj = _fetch_descriptor(service, characteristic, + Android::BluetoothGattDescriptor::CLIENT_CHARACTERISTIC_CONFIG); + + _btGattCallback.set_callback_onCharacteristicChanged(characteristic_obj.getObject(), + [callback](std::vector data) { + ByteArray payload(data.begin(), data.end()); + callback(payload); + }); + bool success = _gatt.setCharacteristicNotification(characteristic_obj, true); + if (!success) { + throw SimpleBLE::Exception::OperationFailed("Failed to subscribe to characteristic " + characteristic); + } + + _btGattCallback.set_flag_descriptorWritePending(descriptor_obj.getObject().get()); + descriptor_obj.setValue(Android::BluetoothGattDescriptor::ENABLE_NOTIFICATION_VALUE); + if (!_gatt.writeDescriptor(descriptor_obj)) { + throw SimpleBLE::Exception::OperationFailed("Failed to write descriptor for characteristic " + characteristic); + } + _btGattCallback.wait_flag_descriptorWritePending(descriptor_obj.getObject().get()); +} + +void PeripheralBase::indicate(BluetoothUUID const& service, BluetoothUUID const& characteristic, + std::function callback) { + auto msg = "Subscribing to characteristic " + characteristic; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg.c_str()); + + auto characteristic_obj = _fetch_characteristic(service, characteristic); + auto descriptor_obj = _fetch_descriptor(service, characteristic, + Android::BluetoothGattDescriptor::CLIENT_CHARACTERISTIC_CONFIG); + + _btGattCallback.set_callback_onCharacteristicChanged(characteristic_obj.getObject(), + [callback](std::vector data) { + ByteArray payload(data.begin(), data.end()); + callback(payload); + }); + bool success = _gatt.setCharacteristicNotification(characteristic_obj, true); + if (!success) { + throw SimpleBLE::Exception::OperationFailed("Failed to subscribe to characteristic " + characteristic); + } + + _btGattCallback.set_flag_descriptorWritePending(descriptor_obj.getObject().get()); + descriptor_obj.setValue(Android::BluetoothGattDescriptor::ENABLE_INDICATION_VALUE); + if (!_gatt.writeDescriptor(descriptor_obj)) { + throw SimpleBLE::Exception::OperationFailed("Failed to write descriptor for characteristic " + characteristic); + } + _btGattCallback.wait_flag_descriptorWritePending(descriptor_obj.getObject().get()); +} + +void PeripheralBase::unsubscribe(BluetoothUUID const& service, BluetoothUUID const& characteristic) { + auto msg = "Unsubscribing from characteristic " + characteristic; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg.c_str()); + + auto characteristic_obj = _fetch_characteristic(service, characteristic); + auto descriptor_obj = _fetch_descriptor(service, characteristic, + Android::BluetoothGattDescriptor::CLIENT_CHARACTERISTIC_CONFIG); + + _btGattCallback.set_flag_descriptorWritePending(descriptor_obj.getObject().get()); + descriptor_obj.setValue(Android::BluetoothGattDescriptor::DISABLE_NOTIFICATION_VALUE); + if (!_gatt.writeDescriptor(descriptor_obj)) { + throw SimpleBLE::Exception::OperationFailed("Failed to write descriptor for characteristic " + characteristic); + } + _btGattCallback.wait_flag_descriptorWritePending(descriptor_obj.getObject().get()); + + _btGattCallback.clear_callback_onCharacteristicChanged(characteristic_obj.getObject()); + bool success = _gatt.setCharacteristicNotification(characteristic_obj, true); + if (!success) { + throw SimpleBLE::Exception::OperationFailed("Failed to subscribe to characteristic " + characteristic); + } +} + +ByteArray PeripheralBase::read(BluetoothUUID const& service, BluetoothUUID const& characteristic, + BluetoothUUID const& descriptor) { + auto msg = "Reading descriptor " + descriptor; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg.c_str()); + + auto descriptor_obj = _fetch_descriptor(service, characteristic, descriptor); + + _btGattCallback.set_flag_descriptorReadPending(descriptor_obj.getObject().get()); + if (!_gatt.readDescriptor(descriptor_obj)) { + throw SimpleBLE::Exception::OperationFailed("Failed to read descriptor " + descriptor); + } + + auto value = _btGattCallback.wait_flag_descriptorReadPending(descriptor_obj.getObject().get()); + return ByteArray(value.begin(), value.end()); +} + +void PeripheralBase::write(BluetoothUUID const& service, BluetoothUUID const& characteristic, + BluetoothUUID const& descriptor, ByteArray const& data) { + auto msg = "Writing descriptor " + descriptor; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg.c_str()); + + auto descriptor_obj = _fetch_descriptor(service, characteristic, descriptor); + + _btGattCallback.set_flag_descriptorWritePending(descriptor_obj.getObject().get()); + descriptor_obj.setValue(std::vector(data.begin(), data.end())); + if (!_gatt.writeDescriptor(descriptor_obj)) { + throw SimpleBLE::Exception::OperationFailed("Failed to write descriptor " + descriptor); + } + _btGattCallback.wait_flag_descriptorWritePending(descriptor_obj.getObject().get()); +} + +void PeripheralBase::set_callback_on_connected(std::function on_connected) { + if (on_connected) { + callback_on_connected_.load(std::move(on_connected)); + } else { + callback_on_connected_.unload(); + } +} + +void PeripheralBase::set_callback_on_disconnected(std::function on_disconnected) { + if (on_disconnected) { + callback_on_disconnected_.load(std::move(on_disconnected)); + } else { + callback_on_disconnected_.unload(); + } +} + +// NOTE: This approach to retrieve the characteristic and descriptor objects is not ideal, as it involves +// iterating on Java objects and returning a copy of the desired object. This will be improved in the future +// if performance becomes a bottleneck. +Android::BluetoothGattCharacteristic PeripheralBase::_fetch_characteristic(const BluetoothUUID& service_uuid, + const BluetoothUUID& characteristic_uuid) { + for (auto& service : _services) { + if (service.getUuid() == service_uuid) { + for (auto& characteristic : service.getCharacteristics()) { + if (characteristic.getUuid() == characteristic_uuid) { + return characteristic; + } + throw SimpleBLE::Exception::CharacteristicNotFound(characteristic_uuid); + } + } + } + + throw SimpleBLE::Exception::ServiceNotFound(service_uuid); +} + +Android::BluetoothGattDescriptor PeripheralBase::_fetch_descriptor(const BluetoothUUID& service_uuid, + const BluetoothUUID& characteristic_uuid, + const BluetoothUUID& descriptor_uuid) { + for (auto& service : _services) { + if (service.getUuid() == service_uuid) { + for (auto& characteristic : service.getCharacteristics()) { + if (characteristic.getUuid() == characteristic_uuid) { + for (auto& descriptor : characteristic.getDescriptors()) { + if (descriptor.getUuid() == descriptor_uuid) { + return descriptor; + } + throw SimpleBLE::Exception::DescriptorNotFound(descriptor_uuid); + } + } + throw SimpleBLE::Exception::CharacteristicNotFound(characteristic_uuid); + } + } + } + throw SimpleBLE::Exception::ServiceNotFound(service_uuid); +} \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/PeripheralBase.h b/third_party/SimpleBLE/simpleble/src/backends/android/PeripheralBase.h new file mode 100644 index 000000000..3a6564e00 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/PeripheralBase.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include + +namespace SimpleBLE { + +class PeripheralBase { + public: + PeripheralBase(Android::ScanResult scan_result); + virtual ~PeripheralBase(); + + void* underlying() const; + + std::string identifier(); + BluetoothAddress address(); + BluetoothAddressType address_type(); + int16_t rssi(); + int16_t tx_power(); + uint16_t mtu(); + + void connect(); + void disconnect(); + bool is_connected(); + bool is_connectable(); + bool is_paired(); + void unpair(); + + std::vector services(); + std::vector advertised_services(); + std::map manufacturer_data(); + + // clang-format off + ByteArray read(BluetoothUUID const& service, BluetoothUUID const& characteristic); + void write_request(BluetoothUUID const& service, BluetoothUUID const& characteristic, ByteArray const& data); + void write_command(BluetoothUUID const& service, BluetoothUUID const& characteristic, ByteArray const& data); + void notify(BluetoothUUID const& service, BluetoothUUID const& characteristic, std::function callback); + void indicate(BluetoothUUID const& service, BluetoothUUID const& characteristic, std::function callback); + void unsubscribe(BluetoothUUID const& service, BluetoothUUID const& characteristic); + + ByteArray read(BluetoothUUID const& service, BluetoothUUID const& characteristic, BluetoothUUID const& descriptor); + void write(BluetoothUUID const& service, BluetoothUUID const& characteristic, BluetoothUUID const& descriptor, ByteArray const& data); + // clang-format on + + void set_callback_on_connected(std::function on_connected); + void set_callback_on_disconnected(std::function on_disconnected); + + void update_advertising_data(Android::ScanResult scan_result); + + private: + + Android::Bridge::BluetoothGattCallback _btGattCallback; + Android::BluetoothDevice _device; + Android::BluetoothGatt _gatt; + std::vector _services; + + kvn::safe_callback callback_on_connected_; + kvn::safe_callback callback_on_disconnected_; + + Android::BluetoothGattCharacteristic _fetch_characteristic(const BluetoothUUID& service_uuid, + const BluetoothUUID& characteristic_uuid); + Android::BluetoothGattDescriptor _fetch_descriptor(const BluetoothUUID& service_uuid, + const BluetoothUUID& characteristic_uuid, + const BluetoothUUID& descriptor_uuid); + +}; + +} // namespace SimpleBLE diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothDevice.cpp b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothDevice.cpp new file mode 100644 index 000000000..d29d2cae3 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothDevice.cpp @@ -0,0 +1,46 @@ +#include "BluetoothDevice.h" + +namespace SimpleBLE { +namespace Android { + +JNI::Class BluetoothDevice::_cls; +jmethodID BluetoothDevice::_method_getAddress; +jmethodID BluetoothDevice::_method_getName; +jmethodID BluetoothDevice::_method_connectGatt; + +void BluetoothDevice::initialize() { + JNI::Env env; + + if (_cls.get() == nullptr) { + _cls = env.find_class("android/bluetooth/BluetoothDevice"); + } + + if (!_method_getAddress) { + _method_getAddress = env->GetMethodID(_cls.get(), "getAddress", "()Ljava/lang/String;"); + } + + if (!_method_getName) { + _method_getName = env->GetMethodID(_cls.get(), "getName", "()Ljava/lang/String;"); + } + + if (!_method_connectGatt) { + _method_connectGatt = env->GetMethodID( + _cls.get(), "connectGatt", + "(Landroid/content/Context;ZLandroid/bluetooth/BluetoothGattCallback;)Landroid/bluetooth/BluetoothGatt;"); + } +} + +BluetoothDevice::BluetoothDevice(JNI::Object obj) : _obj(obj) { + initialize(); +}; + +std::string BluetoothDevice::getAddress() { return _obj.call_string_method(_method_getAddress); } + +std::string BluetoothDevice::getName() { return _obj.call_string_method(_method_getName); } + +BluetoothGatt BluetoothDevice::connectGatt(bool autoConnect, Bridge::BluetoothGattCallback& callback) { + return BluetoothGatt(_obj.call_object_method(_method_connectGatt, nullptr, autoConnect, callback.get())); +} + +} // namespace Android +} // namespace SimpleBLE diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothDevice.h b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothDevice.h new file mode 100644 index 000000000..b38fc6c02 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothDevice.h @@ -0,0 +1,33 @@ +#pragma once + +#include "BluetoothGatt.h" + +#include "jni/Common.hpp" +#include "bridge/BluetoothGattCallback.h" + +namespace SimpleBLE { +namespace Android { + +class BluetoothDevice { +public: + BluetoothDevice(JNI::Object obj); + + std::string getAddress(); + std::string getName(); + + BluetoothGatt connectGatt(bool autoConnect, Bridge::BluetoothGattCallback& callback); + +private: + static JNI::Class _cls; + static jmethodID _method_getAddress; + static jmethodID _method_getName; + static jmethodID _method_connectGatt; + + static void initialize(); + + JNI::Object _obj; + +}; + +} // namespace Android +} // namespace SimpleBLE diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGatt.cpp b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGatt.cpp new file mode 100644 index 000000000..2fefc5c75 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGatt.cpp @@ -0,0 +1,146 @@ +#include "BluetoothGatt.h" +#include +#include + +namespace SimpleBLE { +namespace Android { + +JNI::Class BluetoothGatt::_cls; +jmethodID BluetoothGatt::_method_close = nullptr; +jmethodID BluetoothGatt::_method_connect = nullptr; +jmethodID BluetoothGatt::_method_disconnect = nullptr; +jmethodID BluetoothGatt::_method_discoverServices = nullptr; +jmethodID BluetoothGatt::_method_readCharacteristic = nullptr; +jmethodID BluetoothGatt::_method_readDescriptor = nullptr; +jmethodID BluetoothGatt::_method_setCharacteristicNotification = nullptr; +jmethodID BluetoothGatt::_method_writeCharacteristic = nullptr; +jmethodID BluetoothGatt::_method_writeDescriptor = nullptr; + +void BluetoothGatt::initialize() { + JNI::Env env; + + if (_cls.get() == nullptr) { + _cls = env.find_class("android/bluetooth/BluetoothGatt"); + } + + if (!_method_close) { + _method_close = env->GetMethodID(_cls.get(), "close", "()V"); + } + + if (!_method_connect) { + _method_connect = env->GetMethodID(_cls.get(), "connect", "()Z"); + } + + if (!_method_disconnect) { + _method_disconnect = env->GetMethodID(_cls.get(), "disconnect", "()V"); + } + + if (!_method_discoverServices) { + _method_discoverServices = env->GetMethodID(_cls.get(), "discoverServices", "()Z"); + } + + if (!_method_readCharacteristic) { + _method_readCharacteristic = env->GetMethodID(_cls.get(), "readCharacteristic", + "(Landroid/bluetooth/BluetoothGattCharacteristic;)Z"); + } + + if (!_method_readDescriptor) { + _method_readDescriptor = env->GetMethodID(_cls.get(), "readDescriptor", + "(Landroid/bluetooth/BluetoothGattDescriptor;)Z"); + } + + if (!_method_setCharacteristicNotification) { + _method_setCharacteristicNotification = env->GetMethodID(_cls.get(), "setCharacteristicNotification", + "(Landroid/bluetooth/BluetoothGattCharacteristic;Z)Z"); + } + + if (!_method_writeCharacteristic) { + _method_writeCharacteristic = env->GetMethodID(_cls.get(), "writeCharacteristic", + "(Landroid/bluetooth/BluetoothGattCharacteristic;)Z"); + } + + if (!_method_writeDescriptor) { + _method_writeDescriptor = env->GetMethodID(_cls.get(), "writeDescriptor", + "(Landroid/bluetooth/BluetoothGattDescriptor;)Z"); + } +} + +BluetoothGatt::BluetoothGatt() { initialize(); } + +BluetoothGatt::BluetoothGatt(JNI::Object obj) : BluetoothGatt() { _obj = obj; } + +void BluetoothGatt::close() { + if (!_obj) return; + + _obj.call_void_method(_method_close); +} + +bool BluetoothGatt::connect() { + if (!_obj) return false; + + return _obj.call_boolean_method(_method_connect); +} + +void BluetoothGatt::disconnect() { + if (!_obj) return; + + _obj.call_void_method(_method_disconnect); +} + +bool BluetoothGatt::discoverServices() { + if (!_obj) return false; + + return _obj.call_boolean_method(_method_discoverServices); +} + +std::vector BluetoothGatt::getServices() { + if (!_obj) return std::vector(); + + JNI::Object services = _obj.call_object_method("getServices", "()Ljava/util/List;"); + if (!services) return std::vector(); + + // TODO: We should create a List class type and cache method IDs. + std::vector result; + JNI::Object iterator = services.call_object_method("iterator", "()Ljava/util/Iterator;"); + while (iterator.call_boolean_method("hasNext", "()Z")) { + JNI::Object service = iterator.call_object_method("next", "()Ljava/lang/Object;"); + + if (!service) continue; + result.push_back(BluetoothGattService(service)); + } + + return result; +} + +bool BluetoothGatt::readCharacteristic(BluetoothGattCharacteristic characteristic) { + if (!_obj) return false; + + return _obj.call_boolean_method(_method_readCharacteristic, characteristic.getObject().get()); +} + +bool BluetoothGatt::readDescriptor(BluetoothGattDescriptor descriptor) { + if (!_obj) return false; + + return _obj.call_boolean_method(_method_readDescriptor, descriptor.getObject().get()); +} + +bool BluetoothGatt::setCharacteristicNotification(BluetoothGattCharacteristic characteristic, bool enable) { + if (!_obj) return false; + + return _obj.call_boolean_method(_method_setCharacteristicNotification, characteristic.getObject().get(), enable); +} + +bool BluetoothGatt::writeCharacteristic(BluetoothGattCharacteristic characteristic) { + if (!_obj) return false; + + return _obj.call_boolean_method(_method_writeCharacteristic, characteristic.getObject().get()); +} + +bool BluetoothGatt::writeDescriptor(BluetoothGattDescriptor descriptor) { + if (!_obj) return false; + + return _obj.call_boolean_method(_method_writeDescriptor, descriptor.getObject().get()); +} + +} // namespace Android +} // namespace SimpleBLE diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGatt.h b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGatt.h new file mode 100644 index 000000000..5fbeabc82 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGatt.h @@ -0,0 +1,64 @@ +#pragma once + +#include "BluetoothGattService.h" +#include "jni/Common.hpp" + +#include +#include + +namespace SimpleBLE { +namespace Android { + +class BluetoothGatt { + public: + BluetoothGatt(); + BluetoothGatt(JNI::Object obj); + + void close(); + bool connect(); + void disconnect(); + bool discoverServices(); + std::vector getServices(); + + // void abortReliableWrite(BluetoothDevice mDevice); + // void abortReliableWrite(); + // bool beginReliableWrite(); + // bool executeReliableWrite(); + // std::list getConnectedDevices(); + // int getConnectionState(BluetoothDevice device); + // BluetoothDevice getDevice(); + // std::list getDevicesMatchingConnectionStates(std::vector& states); + // BluetoothGattService getService(UUID uuid); + // std::list getServices(); + bool readCharacteristic(BluetoothGattCharacteristic characteristic); + bool readDescriptor(BluetoothGattDescriptor descriptor); + // void readPhy(); + // bool readRemoteRssi(); + // bool requestConnectionPriority(int connectionPriority); + // bool requestMtu(int mtu); + bool setCharacteristicNotification(BluetoothGattCharacteristic characteristic, bool enable); + // void setPreferredPhy(int txPhy, int rxPhy, int phyOptions); + bool writeCharacteristic(BluetoothGattCharacteristic characteristic); + // int writeCharacteristic(BluetoothGattCharacteristic characteristic, std::vector& value, int writeType); + bool writeDescriptor(BluetoothGattDescriptor descriptor); + // int writeDescriptor(BluetoothGattDescriptor descriptor, std::vector& value); + + private: + static JNI::Class _cls; + static jmethodID _method_close; + static jmethodID _method_connect; + static jmethodID _method_disconnect; + static jmethodID _method_discoverServices; + static jmethodID _method_readCharacteristic; + static jmethodID _method_readDescriptor; + static jmethodID _method_setCharacteristicNotification; + static jmethodID _method_writeCharacteristic; + static jmethodID _method_writeDescriptor; + + static void initialize(); + + JNI::Object _obj; +}; + +} // namespace Android +} // namespace SimpleBLE diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattCharacteristic.cpp b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattCharacteristic.cpp new file mode 100644 index 000000000..697e1515c --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattCharacteristic.cpp @@ -0,0 +1,144 @@ +// +// Created by Kevin Dewald on 5/17/24. +// + +#include "BluetoothGattCharacteristic.h" +#include "UUID.h" +#include "jni/Types.h" + +namespace SimpleBLE { +namespace Android { + +JNI::Class BluetoothGattCharacteristic::_cls; +jmethodID BluetoothGattCharacteristic::_method_addDescriptor = nullptr; +jmethodID BluetoothGattCharacteristic::_method_getDescriptor = nullptr; +jmethodID BluetoothGattCharacteristic::_method_getDescriptors = nullptr; +jmethodID BluetoothGattCharacteristic::_method_getInstanceId = nullptr; +jmethodID BluetoothGattCharacteristic::_method_getPermissions = nullptr; +jmethodID BluetoothGattCharacteristic::_method_getProperties = nullptr; +jmethodID BluetoothGattCharacteristic::_method_getService = nullptr; +jmethodID BluetoothGattCharacteristic::_method_getUuid = nullptr; +jmethodID BluetoothGattCharacteristic::_method_getWriteType = nullptr; +jmethodID BluetoothGattCharacteristic::_method_setWriteType = nullptr; +jmethodID BluetoothGattCharacteristic::_method_setValue = nullptr; + +void BluetoothGattCharacteristic::initialize() { + JNI::Env env; + + if (_cls.get() == nullptr) { + _cls = env.find_class("android/bluetooth/BluetoothGattCharacteristic"); + } + + if (!_method_addDescriptor) { + _method_addDescriptor = env->GetMethodID(_cls.get(), "addDescriptor", + "(Landroid/bluetooth/BluetoothGattDescriptor;)Z"); + } + + if (!_method_getDescriptor) { + _method_getDescriptor = env->GetMethodID(_cls.get(), "getDescriptor", + "(Ljava/util/UUID;)Landroid/bluetooth/BluetoothGattDescriptor;"); + } + + if (!_method_getDescriptors) { + _method_getDescriptors = env->GetMethodID(_cls.get(), "getDescriptors", "()Ljava/util/List;"); + } + + if (!_method_getInstanceId) { + _method_getInstanceId = env->GetMethodID(_cls.get(), "getInstanceId", "()I"); + } + + if (!_method_getPermissions) { + _method_getPermissions = env->GetMethodID(_cls.get(), "getPermissions", "()I"); + } + + if (!_method_getProperties) { + _method_getProperties = env->GetMethodID(_cls.get(), "getProperties", "()I"); + } + + if (!_method_getService) { + _method_getService = env->GetMethodID(_cls.get(), "getService", "()Landroid/bluetooth/BluetoothGattService;"); + } + + if (!_method_getUuid) { + _method_getUuid = env->GetMethodID(_cls.get(), "getUuid", "()Ljava/util/UUID;"); + } + + if (!_method_getWriteType) { + _method_getWriteType = env->GetMethodID(_cls.get(), "getWriteType", "()I"); + } + + if (!_method_setWriteType) { + _method_setWriteType = env->GetMethodID(_cls.get(), "setWriteType", "(I)V"); + } + + if (!_method_setValue) { + _method_setValue = env->GetMethodID(_cls.get(), "setValue", "([B)Z"); + } +} + +BluetoothGattCharacteristic::BluetoothGattCharacteristic() { initialize(); } + +BluetoothGattCharacteristic::BluetoothGattCharacteristic(JNI::Object obj) : BluetoothGattCharacteristic() { + _obj = obj; +} + +// bool BluetoothGattCharacteristic::addDescriptor(BluetoothGattDescriptor descriptor) { +// return _obj.call_boolean_method(_method_addDescriptor, descriptor.getObject()); +// } +// +// BluetoothGattDescriptor BluetoothGattCharacteristic::getDescriptor(std::string uuid) { +// JNI::Env env; +// JNI::Object descObj = _obj.call_object_method(_method_getDescriptor, env->NewStringUTF(uuid.c_str())); +// return BluetoothGattDescriptor(descObj); +// } +// +std::vector BluetoothGattCharacteristic::getDescriptors() { + if (!_obj) return std::vector(); + + JNI::Object descriptors = _obj.call_object_method(_method_getDescriptors); + if (!descriptors) return std::vector(); + + std::vector result; + JNI::Object iterator = descriptors.call_object_method("iterator", "()Ljava/util/Iterator;"); + while (iterator.call_boolean_method("hasNext", "()Z")) { + JNI::Object descriptor = iterator.call_object_method("next", "()Ljava/lang/Object;"); + + if (!descriptor) continue; + result.push_back(BluetoothGattDescriptor(descriptor)); + } + + return result; +} + +int BluetoothGattCharacteristic::getInstanceId() { return _obj.call_int_method(_method_getInstanceId); } + +int BluetoothGattCharacteristic::getPermissions() { return _obj.call_int_method(_method_getPermissions); } + +int BluetoothGattCharacteristic::getProperties() { return _obj.call_int_method(_method_getProperties); } + +std::string BluetoothGattCharacteristic::getUuid() { + if (!_obj) return ""; + + JNI::Object uuidObj = _obj.call_object_method(_method_getUuid); + if (!uuidObj) return ""; + + return UUID(uuidObj).toString(); +} + +int BluetoothGattCharacteristic::getWriteType() { return _obj.call_int_method(_method_getWriteType); } + +void BluetoothGattCharacteristic::setWriteType(int writeType) { + _obj.call_void_method(_method_setWriteType, writeType); +} + +bool BluetoothGattCharacteristic::setValue(const std::vector& value) { + JNI::Env env; + jbyteArray array = JNI::Types::toJByteArray(value); + + bool result = _obj.call_boolean_method(_method_setValue, array); + env->DeleteLocalRef(array); + return result; +} + +} // namespace Android +} // namespace SimpleBLE \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattCharacteristic.h b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattCharacteristic.h new file mode 100644 index 000000000..507db989e --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattCharacteristic.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include "BluetoothGattDescriptor.h" +#include "jni/Common.hpp" + +namespace SimpleBLE { +namespace Android { + +class BluetoothGattCharacteristic { + public: + BluetoothGattCharacteristic(); + BluetoothGattCharacteristic(JNI::Object obj); + + // bool addDescriptor(BluetoothGattDescriptor descriptor); + // BluetoothGattDescriptor getDescriptor(std::string uuid); + std::vector getDescriptors(); + int getInstanceId(); + int getPermissions(); + int getProperties(); + std::string getUuid(); + int getWriteType(); + void setWriteType(int writeType); + bool setValue(const std::vector& value); + + JNI::Object getObject() const { return _obj; } + + static const int PROPERTY_INDICATE = 0x00000020; + static const int PROPERTY_NOTIFY = 0x00000010; + static const int PROPERTY_READ = 0x00000002; + static const int PROPERTY_WRITE = 0x00000008; + static const int PROPERTY_WRITE_NO_RESPONSE = 0x00000004; + + static const int WRITE_TYPE_DEFAULT = 2; + static const int WRITE_TYPE_NO_RESPONSE = 1; + + private: + JNI::Object _obj; + static JNI::Class _cls; + static jmethodID _method_addDescriptor; + static jmethodID _method_getDescriptor; + static jmethodID _method_getDescriptors; + static jmethodID _method_getInstanceId; + static jmethodID _method_getPermissions; + static jmethodID _method_getProperties; + static jmethodID _method_getService; + static jmethodID _method_getUuid; + static jmethodID _method_getWriteType; + static jmethodID _method_setWriteType; + static jmethodID _method_setValue; + + void initialize(); +}; + +} // namespace Android +} // namespace SimpleBLE diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattDescriptor.cpp b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattDescriptor.cpp new file mode 100644 index 000000000..0274c77e5 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattDescriptor.cpp @@ -0,0 +1,70 @@ +#include "BluetoothGattDescriptor.h" +#include "UUID.h" + +namespace SimpleBLE { +namespace Android { + +JNI::Class BluetoothGattDescriptor::_cls; +jmethodID BluetoothGattDescriptor::_method_getUuid = nullptr; +jmethodID BluetoothGattDescriptor::_method_getValue = nullptr; +jmethodID BluetoothGattDescriptor::_method_setValue = nullptr; + +const std::string BluetoothGattDescriptor::CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb"; +const std::vector BluetoothGattDescriptor::DISABLE_NOTIFICATION_VALUE = {0x00, 0x00}; +const std::vector BluetoothGattDescriptor::ENABLE_NOTIFICATION_VALUE = {0x01, 0x00}; +const std::vector BluetoothGattDescriptor::ENABLE_INDICATION_VALUE = {0x02, 0x00}; + +void BluetoothGattDescriptor::initialize() { + JNI::Env env; + + if (_cls.get() == nullptr) { + _cls = env.find_class("android/bluetooth/BluetoothGattDescriptor"); + } + + if (!_method_getUuid) { + _method_getUuid = env->GetMethodID(_cls.get(), "getUuid", "()Ljava/util/UUID;"); + } + + if (!_method_getValue) { + _method_getValue = env->GetMethodID(_cls.get(), "getValue", "()[B"); + } + + if (!_method_setValue) { + _method_setValue = env->GetMethodID(_cls.get(), "setValue", "([B)Z"); + } +} + +BluetoothGattDescriptor::BluetoothGattDescriptor() { initialize(); } + +BluetoothGattDescriptor::BluetoothGattDescriptor(JNI::Object obj) : BluetoothGattDescriptor() { _obj = obj; } + +std::string BluetoothGattDescriptor::getUuid() { + if (!_obj) return ""; + + JNI::Object uuidObj = _obj.call_object_method(_method_getUuid); + if (!uuidObj) return ""; + + return UUID(uuidObj).toString(); +} + +std::vector BluetoothGattDescriptor::getValue() { + if (!_obj) return {}; + + return _obj.call_byte_array_method(_method_getValue); +} + +bool BluetoothGattDescriptor::setValue(const std::vector &value) { + if (!_obj) return false; + + JNI::Env env; + jbyteArray jbyteArray_obj = env->NewByteArray(value.size()); + env->SetByteArrayRegion(jbyteArray_obj, 0, value.size(), reinterpret_cast(value.data())); + + bool result = _obj.call_boolean_method(_method_setValue, jbyteArray_obj); + + env->DeleteLocalRef(jbyteArray_obj); + return result; +} + +} // namespace Android +} // namespace SimpleBLE \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattDescriptor.h b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattDescriptor.h new file mode 100644 index 000000000..73eb7c074 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattDescriptor.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include "jni/Common.hpp" + +namespace SimpleBLE { +namespace Android { + +class BluetoothGattDescriptor { + public: + BluetoothGattDescriptor(); + BluetoothGattDescriptor(JNI::Object obj); + + std::string getUuid(); + + std::vector getValue(); + bool setValue(const std::vector& value); + + JNI::Object getObject() const { return _obj; } + + static const std::string CLIENT_CHARACTERISTIC_CONFIG; + static const std::vector DISABLE_NOTIFICATION_VALUE; + static const std::vector ENABLE_NOTIFICATION_VALUE; + static const std::vector ENABLE_INDICATION_VALUE; + + + private: + JNI::Object _obj; + static JNI::Class _cls; + static jmethodID _method_getUuid; + static jmethodID _method_getValue; + static jmethodID _method_setValue; + + void initialize(); +}; + +} // namespace Android +} // namespace SimpleBLE diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattService.cpp b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattService.cpp new file mode 100644 index 000000000..f50b32b64 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattService.cpp @@ -0,0 +1,122 @@ +// +// Created by Kevin Dewald on 5/17/24. +// + +#include "BluetoothGattService.h" +#include "UUID.h" + +namespace SimpleBLE { +namespace Android { + +JNI::Class BluetoothGattService::_cls; +jmethodID BluetoothGattService::_method_addCharacteristic = nullptr; +jmethodID BluetoothGattService::_method_addService = nullptr; +jmethodID BluetoothGattService::_method_getCharacteristic = nullptr; +jmethodID BluetoothGattService::_method_getCharacteristics = nullptr; +jmethodID BluetoothGattService::_method_getIncludedServices = nullptr; +jmethodID BluetoothGattService::_method_getInstanceId = nullptr; +jmethodID BluetoothGattService::_method_getType = nullptr; +jmethodID BluetoothGattService::_method_getUuid = nullptr; + +void BluetoothGattService::initialize() { + JNI::Env env; + + if (_cls.get() == nullptr) { + _cls = env.find_class("android/bluetooth/BluetoothGattService"); + } + + if (!_method_addCharacteristic) { + _method_addCharacteristic = env->GetMethodID(_cls.get(), "addCharacteristic", + "(Landroid/bluetooth/BluetoothGattCharacteristic;)Z"); + } + + if (!_method_addService) { + _method_addService = env->GetMethodID(_cls.get(), "addService", "(Landroid/bluetooth/BluetoothGattService;)Z"); + } + + if (!_method_getCharacteristic) { + _method_getCharacteristic = env->GetMethodID( + _cls.get(), "getCharacteristic", "(Ljava/util/UUID;)Landroid/bluetooth/BluetoothGattCharacteristic;"); + } + + if (!_method_getCharacteristics) { + _method_getCharacteristics = env->GetMethodID(_cls.get(), "getCharacteristics", "()Ljava/util/List;"); + } + + if (!_method_getIncludedServices) { + _method_getIncludedServices = env->GetMethodID(_cls.get(), "getIncludedServices", "()Ljava/util/List;"); + } + + if (!_method_getInstanceId) { + _method_getInstanceId = env->GetMethodID(_cls.get(), "getInstanceId", "()I"); + } + + if (!_method_getType) { + _method_getType = env->GetMethodID(_cls.get(), "getType", "()I"); + } + + if (!_method_getUuid) { + _method_getUuid = env->GetMethodID(_cls.get(), "getUuid", "()Ljava/util/UUID;"); + } +} + +BluetoothGattService::BluetoothGattService() { initialize(); } + + +BluetoothGattService::BluetoothGattService(JNI::Object obj) : BluetoothGattService() { + _obj = obj; +} + +//bool BluetoothGattService::addCharacteristic(BluetoothGattCharacteristic characteristic) { +// return _obj.call_boolean_method(_method_addCharacteristic, characteristic.getObject()); +//} +// +//bool BluetoothGattService::addService(BluetoothGattService service) { +// return _obj.call_boolean_method(_method_addService, service.getObject()); +//} +// +//BluetoothGattCharacteristic BluetoothGattService::getCharacteristic(std::string uuid) { +// JNI::Env env; +// JNI::Object charObj = _obj.call_object_method(_method_getCharacteristic, env->NewStringUTF(uuid.c_str())); +// return BluetoothGattCharacteristic(charObj); +//} +// +std::vector BluetoothGattService::getCharacteristics() { + if (!_obj) return std::vector(); + + JNI::Object characteristics = _obj.call_object_method(_method_getCharacteristics); + if (!characteristics) return std::vector(); + + std::vector result; + JNI::Object iterator = characteristics.call_object_method("iterator", "()Ljava/util/Iterator;"); + while (iterator.call_boolean_method("hasNext", "()Z")) { + JNI::Object characteristic = iterator.call_object_method("next", "()Ljava/lang/Object;"); + + if (!characteristic) continue; + result.push_back(BluetoothGattCharacteristic(characteristic)); + } + + return result; +} +// +//std::vector BluetoothGattService::getIncludedServices() { +// JNI::Env env; +// JNI::Object listObj = _obj.call_object_method(_method_getIncludedServices); +// return JNI::convert_list(listObj); +//} + +int BluetoothGattService::getInstanceId() { return _obj.call_int_method(_method_getInstanceId); } + +int BluetoothGattService::getType() { return _obj.call_int_method(_method_getType); } + +std::string BluetoothGattService::getUuid() { + if (!_obj) return ""; + + JNI::Object uuidObj = _obj.call_object_method(_method_getUuid); + if (!uuidObj) return ""; + + return UUID(uuidObj).toString(); +} + +} // namespace Android +} // namespace SimpleBLE \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattService.h b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattService.h new file mode 100644 index 000000000..aa3a844b8 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/android/BluetoothGattService.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include "BluetoothGattCharacteristic.h" +#include "jni/Common.hpp" + +namespace SimpleBLE { +namespace Android { + +class BluetoothGattService { + public: + BluetoothGattService(); + BluetoothGattService(JNI::Object obj); + +// bool addCharacteristic(BluetoothGattCharacteristic characteristic); +// bool addService(BluetoothGattService service); +// BluetoothGattCharacteristic getCharacteristic(std::string uuid); + std::vector getCharacteristics(); +// std::vector getIncludedServices(); + int getInstanceId(); + int getType(); + std::string getUuid(); + + JNI::Object getObject() const { return _obj; } + + private: + JNI::Object _obj; + static JNI::Class _cls; + static jmethodID _method_addCharacteristic; + static jmethodID _method_addService; + static jmethodID _method_getCharacteristic; + static jmethodID _method_getCharacteristics; + static jmethodID _method_getIncludedServices; + static jmethodID _method_getInstanceId; + static jmethodID _method_getType; + static jmethodID _method_getUuid; + + void initialize(); +}; + +} // namespace Android +} // namespace SimpleBLE diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/android/ScanResult.cpp b/third_party/SimpleBLE/simpleble/src/backends/android/android/ScanResult.cpp new file mode 100644 index 000000000..e4f125dc2 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/android/ScanResult.cpp @@ -0,0 +1,29 @@ + +#include "ScanResult.h" + +namespace SimpleBLE { +namespace Android { + +JNI::Class ScanResult::_cls; + +void ScanResult::initialize() { + JNI::Env env; + + if (_cls.get() == nullptr) { + _cls = env.find_class("android/bluetooth/le/ScanResult"); + } +} + +ScanResult::ScanResult(jobject j_scan_result) { + initialize(); + _obj = JNI::Object(j_scan_result, _cls.get()); +}; + +BluetoothDevice ScanResult::getDevice() { + return BluetoothDevice(_obj.call_object_method("getDevice", "()Landroid/bluetooth/BluetoothDevice;")); +} + +std::string ScanResult::toString() { return _obj.call_string_method("toString", "()Ljava/lang/String;"); } + +} // namespace Android +} // namespace SimpleBLE \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/android/ScanResult.h b/third_party/SimpleBLE/simpleble/src/backends/android/android/ScanResult.h new file mode 100644 index 000000000..9d32d90f5 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/android/ScanResult.h @@ -0,0 +1,25 @@ +#pragma once + +#include "jni/Common.hpp" + +#include "BluetoothDevice.h" + +namespace SimpleBLE { +namespace Android { + +class ScanResult { + public: + ScanResult(jobject j_scan_result); + + BluetoothDevice getDevice(); + std::string toString(); + + private: + static JNI::Class _cls; + static void initialize(); + + JNI::Object _obj; +}; + +} // namespace Android +} // namespace SimpleBLE diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/android/UUID.cpp b/third_party/SimpleBLE/simpleble/src/backends/android/android/UUID.cpp new file mode 100644 index 000000000..36083b076 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/android/UUID.cpp @@ -0,0 +1,34 @@ +#include "UUID.h" + +namespace SimpleBLE { + namespace Android { + + JNI::Class UUID::_cls; + jmethodID UUID::_method_toString = nullptr; + + void UUID::initialize() { + JNI::Env env; + + if (_cls.get() == nullptr) { + _cls = env.find_class("java/util/UUID"); + } + + if (!_method_toString) { + _method_toString = env->GetMethodID(_cls.get(), "toString", "()Ljava/lang/String;"); + } + } + + UUID::UUID() { initialize(); } + + UUID::UUID(JNI::Object obj) : UUID() { + _obj = obj; + } + + std::string UUID::toString() { + if (!_obj) return ""; + + return _obj.call_string_method(_method_toString); + } + + } // Android +} // SimpleBLE \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/android/UUID.h b/third_party/SimpleBLE/simpleble/src/backends/android/android/UUID.h new file mode 100644 index 000000000..8b601bddd --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/android/UUID.h @@ -0,0 +1,25 @@ +#pragma once + +#include "jni/Common.hpp" + +namespace SimpleBLE { + namespace Android { + + class UUID { + public: + UUID(); + UUID(JNI::Object obj); + + std::string toString(); + + private: + static JNI::Class _cls; + static jmethodID _method_toString; + + static void initialize(); + + JNI::Object _obj; + }; + + } // Android +} // SimpleBLE diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/bridge/BluetoothGattCallback.cpp b/third_party/SimpleBLE/simpleble/src/backends/android/bridge/BluetoothGattCallback.cpp new file mode 100644 index 000000000..a977cf69f --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/bridge/BluetoothGattCallback.cpp @@ -0,0 +1,410 @@ +#include "BluetoothGattCallback.h" +#include + +#include +#include +#include + +namespace SimpleBLE { +namespace Android { +namespace Bridge { + +JNI::Class BluetoothGattCallback::_cls; +std::map BluetoothGattCallback::_map; + +void BluetoothGattCallback::initialize() { + JNI::Env env; + + if (_cls.get() == nullptr) { + _cls = env.find_class("org/simpleble/android/bridge/BluetoothGattCallback"); + } +} + +BluetoothGattCallback::BluetoothGattCallback() : connected(false), mtu(UINT16_MAX) { + initialize(); + + _obj = _cls.call_constructor("()V"); + _map[_obj.get()] = this; +} + +BluetoothGattCallback::~BluetoothGattCallback() { _map.erase(_obj.get()); } + +void BluetoothGattCallback::set_callback_onConnectionStateChange(std::function callback) { + if (callback) { + _callback_onConnectionStateChange.load(callback); + } else { + _callback_onConnectionStateChange.unload(); + } +} + +void BluetoothGattCallback::set_callback_onServicesDiscovered(std::function callback) { + if (callback) { + _callback_onServicesDiscovered.load(callback); + } else { + _callback_onServicesDiscovered.unload(); + } +} + +void BluetoothGattCallback::set_callback_onCharacteristicChanged(JNI::Object characteristic, + std::function)> callback) { + if (callback) { + _callback_onCharacteristicChanged[characteristic].load(callback); + } else { + _callback_onCharacteristicChanged[characteristic].unload(); + } +} + +void BluetoothGattCallback::clear_callback_onCharacteristicChanged(JNI::Object characteristic) { + _callback_onCharacteristicChanged[characteristic].unload(); +} + +void BluetoothGattCallback::set_flag_characteristicWritePending(JNI::Object characteristic) { + auto& flag_data = _flag_characteristicWritePending[characteristic]; + + std::lock_guard lock(flag_data.mtx); + flag_data.flag = true; +} + +void BluetoothGattCallback::clear_flag_characteristicWritePending(JNI::Object characteristic) { + auto& flag_data = _flag_characteristicWritePending[characteristic]; + { + std::lock_guard lock(flag_data.mtx); + flag_data.flag = false; + } + flag_data.cv.notify_all(); +} + +void BluetoothGattCallback::wait_flag_characteristicWritePending(JNI::Object characteristic) { + auto& flag_data = _flag_characteristicWritePending[characteristic]; + std::unique_lock lock(flag_data.mtx); + flag_data.cv.wait_for(lock, std::chrono::seconds(5), [&flag_data] { return !flag_data.flag; }); + + if (flag_data.flag) { + // TODO CLEANUP + throw std::runtime_error("Failed to write characteristic"); + } + + // TODO CLEANUP +} + +void BluetoothGattCallback::set_flag_characteristicReadPending(JNI::Object characteristic) { + auto& flag_data = _flag_characteristicReadPending[characteristic]; + + std::lock_guard lock(flag_data.mtx); + flag_data.flag = true; +} + +void BluetoothGattCallback::clear_flag_characteristicReadPending(JNI::Object characteristic, + std::vector value) { + auto& flag_data = _flag_characteristicReadPending[characteristic]; + { + std::lock_guard lock(flag_data.mtx); + flag_data.flag = false; + flag_data.value = value; + } + flag_data.cv.notify_all(); +} + +std::vector BluetoothGattCallback::wait_flag_characteristicReadPending(JNI::Object characteristic) { + auto& flag_data = _flag_characteristicReadPending[characteristic]; + std::unique_lock lock(flag_data.mtx); + flag_data.cv.wait_for(lock, std::chrono::seconds(5), [&flag_data] { return !flag_data.flag; }); + + if (flag_data.flag) { + // TODO CLEANUP + throw std::runtime_error("Failed to read characteristic"); + } + + return flag_data.value; +} + +void BluetoothGattCallback::set_flag_descriptorWritePending(JNI::Object descriptor) { + auto& flag_data = _flag_descriptorWritePending[descriptor]; + + std::lock_guard lock(flag_data.mtx); + flag_data.flag = true; +} + +void BluetoothGattCallback::clear_flag_descriptorWritePending(JNI::Object descriptor) { + auto& flag_data = _flag_descriptorWritePending[descriptor]; + { + std::lock_guard lock(flag_data.mtx); + flag_data.flag = false; + } + flag_data.cv.notify_all(); +} + +void BluetoothGattCallback::wait_flag_descriptorWritePending(JNI::Object descriptor) { + auto& flag_data = _flag_descriptorWritePending[descriptor]; + std::unique_lock lock(flag_data.mtx); + flag_data.cv.wait_for(lock, std::chrono::seconds(5), [&flag_data] { return !flag_data.flag; }); + + if (flag_data.flag) { + // TODO CLEANUP + throw std::runtime_error("Failed to write descriptor"); + } + + // TODO CLEANUP +} + +void BluetoothGattCallback::set_flag_descriptorReadPending(JNI::Object descriptor) { + auto& flag_data = _flag_descriptorWritePending[descriptor]; + + std::lock_guard lock(flag_data.mtx); + flag_data.flag = true; +} + +void BluetoothGattCallback::clear_flag_descriptorReadPending(JNI::Object descriptor, std::vector value) { + auto& flag_data = _flag_descriptorWritePending[descriptor]; + { + std::lock_guard lock(flag_data.mtx); + flag_data.flag = false; + flag_data.value = value; + } + flag_data.cv.notify_all(); +} + +std::vector BluetoothGattCallback::wait_flag_descriptorReadPending(JNI::Object descriptor) { + auto& flag_data = _flag_descriptorWritePending[descriptor]; + std::unique_lock lock(flag_data.mtx); + flag_data.cv.wait_for(lock, std::chrono::seconds(5), [&flag_data] { return !flag_data.flag; }); + + if (flag_data.flag) { + // TODO CLEANUP + throw std::runtime_error("Failed to read descriptor"); + } + + return flag_data.value; +} + +void BluetoothGattCallback::jni_onConnectionStateChangeCallback(JNIEnv* env, jobject thiz, jobject gatt, jint status, + jint new_state) { + auto msg = fmt::format("onConnectionStateChangeCallback status: {} new_state: {}", status, new_state); + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg.c_str()); + + auto it = BluetoothGattCallback::_map.find(thiz); + if (it == BluetoothGattCallback::_map.end()) { + // TODO: Throw an exception + return; + } + + BluetoothGattCallback* obj = it->second; + const bool connected = new_state == 2; + obj->connected = connected; + SAFE_CALLBACK_CALL(obj->_callback_onConnectionStateChange, connected); +} + +void BluetoothGattCallback::jni_onServicesDiscoveredCallback(JNIEnv* env, jobject thiz, jobject gatt, jint status) { + auto msg = "onServicesDiscoveredCallback"; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg); + + auto it = BluetoothGattCallback::_map.find(thiz); + if (it == BluetoothGattCallback::_map.end()) { + // TODO: Throw an exception + return; + } + + BluetoothGattCallback* obj = it->second; + obj->services_discovered = true; + SAFE_CALLBACK_CALL(obj->_callback_onServicesDiscovered); +} + +void BluetoothGattCallback::jni_onServiceChangedCallback(JNIEnv* env, jobject thiz, jobject gatt) { + // NOTE: If this one gets triggered we're kinda screwed. + auto msg = "onServiceChangedCallback"; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg); + + auto it = BluetoothGattCallback::_map.find(thiz); + if (it == BluetoothGattCallback::_map.end()) { + // TODO: Throw an exception + return; + } +} + +void BluetoothGattCallback::jni_onCharacteristicChangedCallback(JNIEnv* env, jobject thiz, jobject gatt, + jobject characteristic, jbyteArray value) { + auto msg = "onCharacteristicChangedCallback"; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg); + + auto it = BluetoothGattCallback::_map.find(thiz); + if (it == BluetoothGattCallback::_map.end()) { + // TODO: Throw an exception + return; + } + + BluetoothGattCallback* obj = it->second; + auto& callback = obj->_callback_onCharacteristicChanged[characteristic]; + if (callback) { + std::vector data = JNI::Types::fromJByteArray(value); + SAFE_CALLBACK_CALL(callback, data); + } +} + +void BluetoothGattCallback::jni_onCharacteristicReadCallback(JNIEnv* env, jobject thiz, jobject gatt, + jobject characteristic, jbyteArray value, jint status) { + auto msg = "onCharacteristicReadCallback"; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg); + + auto it = BluetoothGattCallback::_map.find(thiz); + if (it == BluetoothGattCallback::_map.end()) { + // TODO: Throw an exception + return; + } + + BluetoothGattCallback* obj = it->second; + obj->clear_flag_characteristicReadPending(characteristic, JNI::Types::fromJByteArray(value)); +} + +void BluetoothGattCallback::jni_onCharacteristicWriteCallback(JNIEnv* env, jobject thiz, jobject gatt, + jobject characteristic, jint status) { + auto msg = "onCharacteristicWriteCallback"; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg); + + auto it = BluetoothGattCallback::_map.find(thiz); + if (it == BluetoothGattCallback::_map.end()) { + // TODO: Throw an exception + return; + } + + BluetoothGattCallback* obj = it->second; + obj->clear_flag_characteristicWritePending(characteristic); +} + +void BluetoothGattCallback::jni_onDescriptorReadCallback(JNIEnv* env, jobject thiz, jobject gatt, jobject descriptor, + jbyteArray value, jint status) { + auto msg = "onDescriptorReadCallback"; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg); + + auto it = BluetoothGattCallback::_map.find(thiz); + if (it == BluetoothGattCallback::_map.end()) { + // TODO: Throw an exception + return; + } + + BluetoothGattCallback* obj = it->second; + obj->clear_flag_descriptorReadPending(descriptor, JNI::Types::fromJByteArray(value)); +} + +void BluetoothGattCallback::jni_onDescriptorWriteCallback(JNIEnv* env, jobject thiz, jobject gatt, jobject descriptor, + jint status) { + auto msg = "onDescriptorWriteCallback"; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg); + + auto it = BluetoothGattCallback::_map.find(thiz); + if (it == BluetoothGattCallback::_map.end()) { + // TODO: Throw an exception + return; + } + + BluetoothGattCallback* obj = it->second; + obj->clear_flag_descriptorWritePending(descriptor); +} + +void BluetoothGattCallback::jni_onMtuChangedCallback(JNIEnv* env, jobject thiz, jobject gatt, jint mtu, jint status) { + auto msg = "onMtuChangedCallback"; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg); + + auto it = BluetoothGattCallback::_map.find(thiz); + if (it != BluetoothGattCallback::_map.end()) { + BluetoothGattCallback* obj = it->second; + obj->mtu = mtu; + } else { + // TODO: Throw an exception + } +} + +void BluetoothGattCallback::jni_onPhyReadCallback(JNIEnv* env, jobject thiz, jobject gatt, jint tx_phy, jint rx_phy, + jint status) { + auto msg = "onPhyReadCallback"; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg); +} + +void BluetoothGattCallback::jni_onPhyUpdateCallback(JNIEnv* env, jobject thiz, jobject gatt, jint tx_phy, jint rx_phy, + jint status) { + auto msg = "onPhyUpdateCallback"; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg); +} + +void BluetoothGattCallback::jni_onReadRemoteRssiCallback(JNIEnv* env, jobject thiz, jobject gatt, jint rssi, + jint status) { + auto msg = "onReadRemoteRssiCallback"; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg); +} + +void BluetoothGattCallback::jni_onReliableWriteCompletedCallback(JNIEnv* env, jobject thiz, jobject gatt, jint status) { + auto msg = "onReliableWriteCompletedCallback"; + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg); +} + +} // namespace Bridge +} // namespace Android +} // namespace SimpleBLE + +extern "C" { +// clang-format off +JNIEXPORT void JNICALL Java_org_simpleble_android_bridge_BluetoothGattCallback_onCharacteristicChangedCallback( + JNIEnv* env, jobject thiz, jobject gatt, jobject characteristic, jbyteArray value) { + SimpleBLE::Android::Bridge::BluetoothGattCallback::jni_onCharacteristicChangedCallback(env, thiz, gatt, characteristic, value); +} + +JNIEXPORT void JNICALL Java_org_simpleble_android_bridge_BluetoothGattCallback_onCharacteristicReadCallback( + JNIEnv* env, jobject thiz, jobject gatt, jobject characteristic, jbyteArray value, jint status) { + SimpleBLE::Android::Bridge::BluetoothGattCallback::jni_onCharacteristicReadCallback(env, thiz, gatt, characteristic, value, status); +} + +JNIEXPORT void JNICALL Java_org_simpleble_android_bridge_BluetoothGattCallback_onCharacteristicWriteCallback( + JNIEnv* env, jobject thiz, jobject gatt, jobject characteristic, jint status) { + SimpleBLE::Android::Bridge::BluetoothGattCallback::jni_onCharacteristicWriteCallback(env, thiz, gatt, characteristic, status); +} + +JNIEXPORT void JNICALL Java_org_simpleble_android_bridge_BluetoothGattCallback_onConnectionStateChangeCallback( + JNIEnv* env, jobject thiz, jobject gatt, jint status, jint new_state) { + SimpleBLE::Android::Bridge::BluetoothGattCallback::jni_onConnectionStateChangeCallback(env, thiz, gatt, status, new_state); +} + +JNIEXPORT void JNICALL Java_org_simpleble_android_bridge_BluetoothGattCallback_onDescriptorReadCallback( + JNIEnv* env, jobject thiz, jobject gatt, jobject descriptor, jbyteArray value, jint status) { + SimpleBLE::Android::Bridge::BluetoothGattCallback::jni_onDescriptorReadCallback(env, thiz, gatt, descriptor, value, status); +} + +JNIEXPORT void JNICALL Java_org_simpleble_android_bridge_BluetoothGattCallback_onDescriptorWriteCallback( + JNIEnv* env, jobject thiz, jobject gatt, jobject descriptor, jint status) { + SimpleBLE::Android::Bridge::BluetoothGattCallback::jni_onDescriptorWriteCallback(env, thiz, gatt, descriptor, status); +} + +JNIEXPORT void JNICALL Java_org_simpleble_android_bridge_BluetoothGattCallback_onMtuChangedCallback( + JNIEnv* env, jobject thiz, jobject gatt, jint mtu, jint status) { + SimpleBLE::Android::Bridge::BluetoothGattCallback::jni_onMtuChangedCallback(env, thiz, gatt, mtu, status); +} + +JNIEXPORT void JNICALL Java_org_simpleble_android_bridge_BluetoothGattCallback_onPhyReadCallback( + JNIEnv* env, jobject thiz, jobject gatt, jint tx_phy, jint rx_phy, jint status) { + SimpleBLE::Android::Bridge::BluetoothGattCallback::jni_onPhyReadCallback(env, thiz, gatt, tx_phy, rx_phy, status); +} + +JNIEXPORT void JNICALL Java_org_simpleble_android_bridge_BluetoothGattCallback_onPhyUpdateCallback( + JNIEnv* env, jobject thiz, jobject gatt, jint tx_phy, jint rx_phy, jint status) { + SimpleBLE::Android::Bridge::BluetoothGattCallback::jni_onPhyUpdateCallback(env, thiz, gatt, tx_phy, rx_phy, status); +} + +JNIEXPORT void JNICALL Java_org_simpleble_android_bridge_BluetoothGattCallback_onReadRemoteRssiCallback( + JNIEnv* env, jobject thiz, jobject gatt, jint rssi, jint status) { + SimpleBLE::Android::Bridge::BluetoothGattCallback::jni_onReadRemoteRssiCallback(env, thiz, gatt, rssi, status); +} + +JNIEXPORT void JNICALL Java_org_simpleble_android_bridge_BluetoothGattCallback_onReliableWriteCompletedCallback( + JNIEnv* env, jobject thiz, jobject gatt, jint status) { + SimpleBLE::Android::Bridge::BluetoothGattCallback::jni_onReliableWriteCompletedCallback(env, thiz, gatt, status); +} + +JNIEXPORT void JNICALL Java_org_simpleble_android_bridge_BluetoothGattCallback_onServiceChangedCallback( + JNIEnv* env, jobject thiz, jobject gatt) { + SimpleBLE::Android::Bridge::BluetoothGattCallback::jni_onServiceChangedCallback(env, thiz, gatt); +} + +JNIEXPORT void JNICALL Java_org_simpleble_android_bridge_BluetoothGattCallback_onServicesDiscoveredCallback( + JNIEnv* env, jobject thiz, jobject gatt, jint status) { + SimpleBLE::Android::Bridge::BluetoothGattCallback::jni_onServicesDiscoveredCallback(env, thiz, gatt, status); +} +// clang-format on +} // extern "C" \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/bridge/BluetoothGattCallback.h b/third_party/SimpleBLE/simpleble/src/backends/android/bridge/BluetoothGattCallback.h new file mode 100644 index 000000000..ffad0e4a3 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/bridge/BluetoothGattCallback.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "jni/Common.hpp" + +namespace SimpleBLE { +namespace Android { +namespace Bridge { + +class BluetoothGattCallback { + public: + BluetoothGattCallback(); + virtual ~BluetoothGattCallback(); + jobject get() { return _obj.get(); } // TODO: Remove once nothing uses this + + void set_callback_onConnectionStateChange(std::function callback); + void set_callback_onServicesDiscovered(std::function callback); + + void set_callback_onCharacteristicChanged(JNI::Object characteristic, + std::function value)> callback); + void clear_callback_onCharacteristicChanged(JNI::Object characteristic); + + void set_flag_characteristicWritePending(JNI::Object characteristic); + void clear_flag_characteristicWritePending(JNI::Object characteristic); + void wait_flag_characteristicWritePending(JNI::Object characteristic); + + void set_flag_characteristicReadPending(JNI::Object characteristic); + void clear_flag_characteristicReadPending(JNI::Object characteristic, std::vector value); + std::vector wait_flag_characteristicReadPending(JNI::Object characteristic); + + void set_flag_descriptorWritePending(JNI::Object descriptor); + void clear_flag_descriptorWritePending(JNI::Object descriptor); + void wait_flag_descriptorWritePending(JNI::Object descriptor); + + void set_flag_descriptorReadPending(JNI::Object descriptor); + void clear_flag_descriptorReadPending(JNI::Object descriptor, std::vector value); + std::vector wait_flag_descriptorReadPending(JNI::Object descriptor); + + bool connected; + bool services_discovered; + uint16_t mtu; + + // Not for public use + // clang-format off + static void jni_onConnectionStateChangeCallback(JNIEnv *env, jobject thiz, jobject gatt, jint status, jint new_state); + static void jni_onServicesDiscoveredCallback(JNIEnv *env, jobject thiz, jobject gatt, jint status); + static void jni_onServiceChangedCallback(JNIEnv *env, jobject thiz, jobject gatt); + + static void jni_onCharacteristicChangedCallback(JNIEnv *env, jobject thiz, jobject gatt, jobject characteristic, jbyteArray value); + static void jni_onCharacteristicReadCallback(JNIEnv *env, jobject thiz, jobject gatt, jobject characteristic, jbyteArray value, jint status); + static void jni_onCharacteristicWriteCallback(JNIEnv *env, jobject thiz, jobject gatt, jobject characteristic, jint status); + + static void jni_onDescriptorReadCallback(JNIEnv *env, jobject thiz, jobject gatt, jobject descriptor, jbyteArray value, jint status); + static void jni_onDescriptorWriteCallback(JNIEnv *env, jobject thiz, jobject gatt, jobject descriptor, jint status); + + static void jni_onMtuChangedCallback(JNIEnv *env, jobject thiz, jobject gatt, jint mtu, jint status); + static void jni_onPhyReadCallback(JNIEnv *env, jobject thiz, jobject gatt, jint txPhy, jint rxPhy, jint status); + static void jni_onPhyUpdateCallback(JNIEnv *env, jobject thiz, jobject gatt, jint txPhy, jint rxPhy, jint status); + static void jni_onReadRemoteRssiCallback(JNIEnv *env, jobject thiz, jobject gatt, jint rssi, jint status); + static void jni_onReliableWriteCompletedCallback(JNIEnv *env, jobject thiz, jobject gatt, jint status); + // clang-format on + + private: + struct FlagData { + bool flag; + std::condition_variable cv; + std::mutex mtx; + std::vector value; + }; + + static JNI::Class _cls; + static std::map _map; + static void initialize(); + + JNI::Object _obj; + + kvn::safe_callback _callback_onConnectionStateChange; + kvn::safe_callback _callback_onServicesDiscovered; + + std::map)>, JNI::JniObjectComparator> + _callback_onCharacteristicChanged; + + std::map _flag_characteristicWritePending; + std::map _flag_characteristicReadPending; + std::map _flag_descriptorWritePending; + std::map _flag_descriptorReadPending; +}; + +} // namespace Bridge +} // namespace Android +} // namespace SimpleBLE \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/bridge/ScanCallback.cpp b/third_party/SimpleBLE/simpleble/src/backends/android/bridge/ScanCallback.cpp new file mode 100644 index 000000000..4b65f4de8 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/bridge/ScanCallback.cpp @@ -0,0 +1,98 @@ +#include +#include +#include +#include "ScanCallback.h" + +namespace SimpleBLE { +namespace Android { +namespace Bridge { + +JNI::Class ScanCallback::_cls; +std::map ScanCallback::_map; + +void ScanCallback::initialize() { + JNI::Env env; + + if (_cls.get() == nullptr) { + _cls = env.find_class("org/simpleble/android/bridge/ScanCallback"); + } +} + +ScanCallback::ScanCallback() { + initialize(); + + _obj = _cls.call_constructor("()V"); + _map[_obj.get()] = this; +} + +ScanCallback::~ScanCallback() { + _map.erase(_obj.get()); +} + +void ScanCallback::set_callback_onScanResult(std::function callback) { + if (callback) { + _callback_onScanResult.load(callback); + } else { + _callback_onScanResult.unload(); + } +} + +void ScanCallback::set_callback_onBatchScanResults(std::function callback) { + if (callback) { + _callback_onBatchScanResults.load(callback); + } else { + _callback_onBatchScanResults.unload(); + } +} + +void ScanCallback::set_callback_onScanFailed(std::function callback) { + if (callback) { + _callback_onScanFailed.load(callback); + } else { + _callback_onScanFailed.unload(); + } +} + +void ScanCallback::jni_onScanResultCallback(JNIEnv *env, jobject thiz, jint callback_type, jobject j_scan_result) { + auto it = ScanCallback::_map.find(thiz); + if (it != ScanCallback::_map.end()) { + ScanCallback* obj = it->second; + Android::ScanResult scan_result(j_scan_result); + + auto msg = fmt::format("onScanResultCallback: {}", scan_result.toString()); + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg.c_str()); + + SAFE_CALLBACK_CALL(obj->_callback_onScanResult, scan_result); + } else { + // TODO: Throw an exception + } +} + +void ScanCallback::jni_onBatchScanResultsCallback(JNIEnv *env, jobject thiz, jobject results) { + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", "onBatchScanResultsCallback"); + // TODO: Implement +} + +void ScanCallback::jni_onScanFailedCallback(JNIEnv *env, jobject thiz, jint error_code) { + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", "onScanFailedCallback"); + // TODO: Implement +} + +} // namespace Bridge +} // namespace Android +} // namespace SimpleBLE + +extern "C" { +// clang-format off +JNIEXPORT void JNICALL Java_org_simpleble_android_bridge_ScanCallback_onScanResultCallback(JNIEnv *env, jobject thiz, jint callback_type, jobject result) { + SimpleBLE::Android::Bridge::ScanCallback::jni_onScanResultCallback(env, thiz, callback_type, result); +} + +JNIEXPORT void JNICALL Java_org_simpleble_android_bridge_ScanCallback_onScanFailedCallback(JNIEnv *env, jobject thiz, jint error_code) { + SimpleBLE::Android::Bridge::ScanCallback::jni_onScanFailedCallback(env, thiz, error_code); +} +JNIEXPORT void JNICALL Java_org_simpleble_android_bridge_ScanCallback_onBatchScanResultsCallback(JNIEnv *env, jobject thiz, jobject results) { + SimpleBLE::Android::Bridge::ScanCallback::jni_onBatchScanResultsCallback(env, thiz, results); +} +// clang-format on +} \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/bridge/ScanCallback.h b/third_party/SimpleBLE/simpleble/src/backends/android/bridge/ScanCallback.h new file mode 100644 index 000000000..2738e72e8 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/bridge/ScanCallback.h @@ -0,0 +1,41 @@ +#pragma once + +#include "jni/Common.hpp" +#include +#include "android/ScanResult.h" +#include + +namespace SimpleBLE { +namespace Android { +namespace Bridge { + +class ScanCallback { + public: + ScanCallback(); + virtual ~ScanCallback(); + jobject get() { return _obj.get(); } // TODO: Remove once nothing uses this + + void set_callback_onScanResult(std::function callback); + void set_callback_onBatchScanResults(std::function callback); + void set_callback_onScanFailed(std::function callback); + + // Not for public use + static void jni_onScanResultCallback(JNIEnv *env, jobject thiz, jint callback_type, jobject result); + static void jni_onBatchScanResultsCallback(JNIEnv *env, jobject thiz, jobject results); + static void jni_onScanFailedCallback(JNIEnv *env, jobject thiz, jint error_code); + + private: + static JNI::Class _cls; + static std::map _map; + static void initialize(); + + JNI::Object _obj; + + kvn::safe_callback _callback_onScanResult; + kvn::safe_callback _callback_onBatchScanResults; + kvn::safe_callback _callback_onScanFailed; +}; + +} // namespace Bridge +} // namespace Android +} // namespace SimpleBLE \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/jni/Common.hpp b/third_party/SimpleBLE/simpleble/src/backends/android/jni/Common.hpp new file mode 100644 index 000000000..0ef6c7757 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/jni/Common.hpp @@ -0,0 +1,321 @@ +#pragma once + +#include +#include +#include +#include + +#include "GlobalRef.hpp" +#include "VM.hpp" + +namespace SimpleBLE { +namespace JNI { + +// Forward declarations +class Class; + +class Object { + public: + Object() = default; + + Object(jobject obj) : _obj(obj) { + JNIEnv* env = VM::env(); + _cls = env->GetObjectClass(obj); + } + + Object(jobject obj, jclass cls) : _obj(obj), _cls(cls) {} + + jobject get() const { return _obj.get(); } + + explicit operator bool() const { + return _obj.get() != nullptr; + } + + jmethodID get_method(const char* name, const char* signature) { + JNIEnv* env = VM::env(); + return env->GetMethodID(_cls.get(), name, signature); + } + + template + Object call_object_method(jmethodID method, Args&&... args) { + JNIEnv* env = VM::env(); + jobject result = env->CallObjectMethod(_obj.get(), method, std::forward(args)...); + jclass resultClass = env->GetObjectClass(result); + return Object(result, resultClass); + } + + template + void call_void_method(jmethodID method, Args&&... args) { + JNIEnv* env = VM::env(); + env->CallVoidMethod(_obj.get(), method, std::forward(args)...); + } + + template + bool call_boolean_method(jmethodID method, Args&&... args) { + JNIEnv* env = VM::env(); + jboolean result = env->CallBooleanMethod(_obj.get(), method, std::forward(args)...); + return result; + } + + template + int call_int_method(jmethodID method, Args&&... args) { + JNIEnv* env = VM::env(); + jint result = env->CallIntMethod(_obj.get(), method, std::forward(args)...); + return result; + } + + template + std::string call_string_method(jmethodID method, Args&&... args) { + JNIEnv* env = VM::env(); + jstring jstr = (jstring)env->CallObjectMethod(_obj.get(), method, std::forward(args)...); + + if (jstr == nullptr) { + return ""; + } + + const char* c_str = env->GetStringUTFChars(jstr, nullptr); + std::string result(c_str); + env->ReleaseStringUTFChars(jstr, c_str); + return result; + } + + template + std::vector call_byte_array_method(jmethodID method, Args&&... args) { + JNIEnv* env = VM::env(); + jbyteArray jarr = (jbyteArray)env->CallObjectMethod(_obj.get(), method, std::forward(args)...); + + if (jarr == nullptr) { + return {}; + } + + jsize len = env->GetArrayLength(jarr); + jbyte* arr = env->GetByteArrayElements(jarr, nullptr); + + std::vector result(arr, arr + len); + + env->ReleaseByteArrayElements(jarr, arr, JNI_ABORT); + return result; + } + + + + template + Object call_object_method(const char* name, const char* signature, Args&&... args) { + JNIEnv* env = VM::env(); + + jmethodID method = env->GetMethodID(_cls.get(), name, signature); + jobject result = env->CallObjectMethod(_obj.get(), method, std::forward(args)...); + + jclass resultClass = env->GetObjectClass(result); + + return Object(result, resultClass); + } + + template + void call_void_method(const char* name, const char* signature, Args&&... args) { + JNIEnv* env = VM::env(); + + jmethodID method = env->GetMethodID(_cls.get(), name, signature); + env->CallVoidMethod(_obj.get(), method, std::forward(args)...); + } + + template + bool call_boolean_method(const char* name, const char* signature, Args&&... args) { + JNIEnv* env = VM::env(); + + jmethodID method = env->GetMethodID(_cls.get(), name, signature); + jboolean result = env->CallBooleanMethod(_obj.get(), method, std::forward(args)...); + + return result; + } + + template + int call_int_method(const char* name, const char* signature, Args&&... args) { + JNIEnv* env = VM::env(); + + jmethodID method = env->GetMethodID(_cls.get(), name, signature); + jint result = env->CallIntMethod(_obj.get(), method, std::forward(args)...); + + return result; + } + + template + std::string call_string_method(const char* name, const char* signature, Args&&... args) { + JNIEnv* env = VM::env(); + + jmethodID method = env->GetMethodID(_cls.get(), name, signature); + jstring jstr = (jstring)env->CallObjectMethod(_obj.get(), method, std::forward(args)...); + + if (jstr == nullptr) { + return ""; + } + + const char* c_str = env->GetStringUTFChars(jstr, nullptr); + std::string result(c_str); + env->ReleaseStringUTFChars(jstr, c_str); + return result; + } + + template + std::vector call_byte_array_method(const char* name, const char* signature, Args&&... args) { + JNIEnv* env = VM::env(); + + jmethodID method = env->GetMethodID(_cls.get(), name, signature); + jbyteArray jarr = (jbyteArray)env->CallObjectMethod(_obj.get(), method, std::forward(args)...); + + if (jarr == nullptr) { + return {}; + } + + jsize len = env->GetArrayLength(jarr); + jbyte* arr = env->GetByteArrayElements(jarr, nullptr); + + std::vector result(arr, arr + len); + + env->ReleaseByteArrayElements(jarr, arr, JNI_ABORT); + return result; + } + + private: + GlobalRef _obj; + GlobalRef _cls; +}; + +class Class { + public: + Class() = default; + Class(jclass cls) : _cls(cls) {} + + jclass get() { return _cls.get(); } + + template + Object call_static_method(const char* name, const char* signature, Args&&... args) { + JNIEnv* env = VM::env(); + + jmethodID method = env->GetStaticMethodID(_cls.get(), name, signature); + jobject obj = env->CallStaticObjectMethod(_cls.get(), method, std::forward(args)...); + + return Object(obj, _cls.get()); + } + + template + Object call_constructor(const char* signature, Args&&... args) { + JNIEnv* env = VM::env(); + + jmethodID method = env->GetMethodID(_cls.get(), "", signature); + jobject obj = env->NewObject(_cls.get(), method, std::forward(args)...); + + return Object(obj, _cls.get()); + } + + void register_natives(const JNINativeMethod* methods, int nMethods) { + JNIEnv* env = VM::env(); + env->RegisterNatives(_cls.get(), methods, nMethods); + } + + private: + GlobalRef _cls; +}; + +class Env { +public: + Env() { _env = VM::env(); } + virtual ~Env() = default; + Env(Env& other) = delete; // Remove the copy constructor + void operator=(const Env&) = delete; // Remove the copy assignment + + JNIEnv* operator->() { return _env; } + + Class find_class(const std::string& name) { + jclass jcls = _env->FindClass(name.c_str()); + if (jcls == nullptr) { + throw std::runtime_error("Failed to find class: " + name); + } + Class cls(jcls); + _env->DeleteLocalRef(jcls); + return cls; + } + +private: + JNIEnv* _env = nullptr; +}; + +// TODO: Move these to their own namespace + +struct JObjectComparator { + bool operator()(const jobject& lhs, const jobject& rhs) const { + if (lhs == nullptr && rhs == nullptr) { + return false; // Both are null, considered equal + } + if (lhs == nullptr) { + return true; // lhs is null, rhs is not null, lhs < rhs + } + if (rhs == nullptr) { + return false; // rhs is null, lhs is not null, lhs > rhs + } + + JNIEnv* env = VM::env(); + if (env->IsSameObject(lhs, rhs)) { + return false; // Both objects are the same + } + + // Use hashCode method to establish a consistent ordering + // TODO: Cache all references statically for this class! + jclass objectClass = env->FindClass("java/lang/Object"); + jmethodID hashCodeMethod = env->GetMethodID(objectClass, "hashCode", "()I"); + + const jobject lhsObject = lhs; + const jobject rhsObject = rhs; + + jint lhsHashCode = env->CallIntMethod(lhsObject, hashCodeMethod); + jint rhsHashCode = env->CallIntMethod(rhsObject, hashCodeMethod); + + return lhsHashCode < rhsHashCode; + + // Use a unique identifier or a pointer value as the final comparison for non-equal objects + return lhs < rhs; // This can still be used for consistent ordering + } +}; + +struct JniObjectComparator { + bool operator()(const Object& lhs, const Object& rhs) const { + // Handle null object comparisons + if (!lhs && !rhs) { + return false; // Both are null, considered equal + } + if (!lhs) { + return true; // lhs is null, rhs is not, lhs < rhs + } + if (!rhs) { + return false; // rhs is null, lhs is not, lhs > rhs + } + + JNIEnv* env = VM::env(); + + // Access the underlying jobject handles from Object instances + jobject lhsObject = lhs.get(); + jobject rhsObject = rhs.get(); + + // Check if both jobject handles refer to the same object + if (env->IsSameObject(lhsObject, rhsObject)) { + return false; // Both objects are the same + } + + // Use hashCode method to establish a consistent ordering + jclass objectClass = env->FindClass("java/lang/Object"); + jmethodID hashCodeMethod = env->GetMethodID(objectClass, "hashCode", "()I"); + + jint lhsHashCode = env->CallIntMethod(lhsObject, hashCodeMethod); + jint rhsHashCode = env->CallIntMethod(rhsObject, hashCodeMethod); + + if (lhsHashCode != rhsHashCode) { + return lhsHashCode < rhsHashCode; // Use hash code for initial comparison + } + + // Use a direct pointer comparison as a fallback for objects with identical hash codes + return lhsObject < rhsObject; // This comparison is consistent within the same execution + } +}; + +} // namespace JNI +} // namespace SimpleBLE diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/jni/GlobalRef.hpp b/third_party/SimpleBLE/simpleble/src/backends/android/jni/GlobalRef.hpp new file mode 100644 index 000000000..5e528778b --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/jni/GlobalRef.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include "VM.hpp" + +namespace SimpleBLE { +namespace JNI { + +template +class GlobalRef { + public: + GlobalRef() = default; + + GlobalRef(T obj) {_obj = (T) VM::env()->NewGlobalRef(obj);} + + GlobalRef(const GlobalRef& other) { + // Custom copy constructor + _obj = (T) VM::env()->NewGlobalRef(other._obj); + } + + GlobalRef& operator=(const GlobalRef& other) { + // Custom copy assignment + if (this != &other) { + if (_obj != nullptr) { + VM::env()->DeleteGlobalRef(_obj); + } + _obj = (T) VM::env()->NewGlobalRef(other._obj); + } + return *this; + } + + ~GlobalRef() { + if (_obj != nullptr) { + VM::env()->DeleteGlobalRef(_obj); + _obj = nullptr; + } + } + + T* operator->() const { return &_obj; } + + T get() const { return _obj; } + + protected: + T _obj = nullptr; +}; + +} // namespace JNI +} // namespace SimpleBLE \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/jni/Types.h b/third_party/SimpleBLE/simpleble/src/backends/android/jni/Types.h new file mode 100644 index 000000000..b499c704b --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/jni/Types.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include "Common.hpp" + +namespace SimpleBLE { + namespace JNI { + namespace Types { + // TODO: Review the inline approach some time in the future. + inline jbyteArray toJByteArray(const std::vector& data) { + JNI::Env env; + jbyteArray array = env->NewByteArray(data.size()); + env->SetByteArrayRegion(array, 0, data.size(), (jbyte*) data.data()); + return array; + } + + inline std::vector fromJByteArray(jbyteArray array) { + JNI::Env env; + jsize length = env->GetArrayLength(array); + std::vector data(length); + env->GetByteArrayRegion(array, 0, length, (jbyte*) data.data()); + return data; + } + } + + } // namespace JNI +} \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/jni/VM.hpp b/third_party/SimpleBLE/simpleble/src/backends/android/jni/VM.hpp new file mode 100644 index 000000000..2c718413e --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/jni/VM.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include + +namespace SimpleBLE { +namespace JNI { + +// Generic class to handle the Java Virtual Machine (JVM) +class VM { + public: + static JavaVM* jvm() { + static std::mutex get_mutex; // Static mutex to ensure thread safety when accessing the logger + std::scoped_lock lock(get_mutex); // Unlock the mutex on function return + static VM instance; // Static instance of the logger to ensure proper lifecycle management + + if (instance._jvm == nullptr) { + jsize count; + if (JNI_GetCreatedJavaVMs(&instance._jvm, 1, &count) != JNI_OK || count == 0) { + throw std::runtime_error("Failed to get the Java Virtual Machine"); + } + } + return instance._jvm; + } + + static JNIEnv* env() { + JNIEnv* env = nullptr; + JavaVM* jvm = VM::jvm(); + if (jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + throw std::runtime_error("Failed to get the JNIEnv"); + } + return env; + } + + private: + VM() = default; + virtual ~VM() = default; + VM(VM& other) = delete; // Remove the copy constructor + void operator=(const VM&) = delete; // Remove the copy assignment + + JavaVM* _jvm = nullptr; +}; + +} // namespace JNI +} // namespace SimpleBLE \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/.gitignore b/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/.gitignore new file mode 100644 index 000000000..aa724b770 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/build.gradle.kts b/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/build.gradle.kts new file mode 100644 index 000000000..9fd04ac00 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("com.android.library") version "8.3.1" +} + +android { + namespace = "org.simpleble.android.bridge" + compileSdk = 34 + + defaultConfig { + minSdk = 31 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/gradle/wrapper/gradle-wrapper.jar b/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..033e24c4cdf41af1ab109bc7f253b2b887023340 GIT binary patch literal 63375 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≫L+crb^Bz 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/gradlew.bat b/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/gradlew.bat new file mode 100644 index 000000000..93e3f59f1 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/settings.gradle.kts b/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/settings.gradle.kts new file mode 100644 index 000000000..3a35432e1 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/settings.gradle.kts @@ -0,0 +1,22 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "simpleble-bridge" diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/src/main/AndroidManifest.xml b/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/src/main/AndroidManifest.xml new file mode 100644 index 000000000..0e0cf2a6c --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/src/main/java/org/simpleble/android/bridge/BluetoothGattCallback.java b/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/src/main/java/org/simpleble/android/bridge/BluetoothGattCallback.java new file mode 100644 index 000000000..87e84f7bb --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/src/main/java/org/simpleble/android/bridge/BluetoothGattCallback.java @@ -0,0 +1,141 @@ +package org.simpleble.android.bridge; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.util.Log; + +public class BluetoothGattCallback extends android.bluetooth.BluetoothGattCallback { + + public BluetoothGattCallback() {} + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + // NOTE: This method has been deprecated on API 33, but we're still using API 31, so we need to support this. + super.onCharacteristicChanged(gatt, characteristic); + onCharacteristicChangedCallback(gatt, characteristic, characteristic.getValue()); + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) { + // NOTE: This method is only available from API 33 onwards. + super.onCharacteristicChanged(gatt, characteristic, value); + onCharacteristicChangedCallback(gatt, characteristic, value); + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + // NOTE: This method has been deprecated on API 33, but we're still using API 31, so we need to support this. + super.onCharacteristicRead(gatt, characteristic, status); + onCharacteristicReadCallback(gatt, characteristic, characteristic.getValue(), status); + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value, int status) { + // NOTE: This method is only available from API 33 onwards. + super.onCharacteristicRead(gatt, characteristic, value, status); + onCharacteristicReadCallback(gatt, characteristic, value, status); + } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + super.onCharacteristicWrite(gatt, characteristic, status); + onCharacteristicWriteCallback(gatt, characteristic, status); + } + + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + super.onConnectionStateChange(gatt, status, newState); + onConnectionStateChangeCallback(gatt, status, newState); + } + + // NOTE: This method is only available from API 33 onwards + @Override + public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + // NOTE: This method has been deprecated on API 33, but we're still using API 31, so we need to support this. + super.onDescriptorRead(gatt, descriptor, status); + onDescriptorReadCallback(gatt, descriptor, descriptor.getValue(), status); + } + + @Override + public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status, byte[] value) { + // NOTE: This method is only available from API 33 onwards. + super.onDescriptorRead(gatt, descriptor, status, value); + onDescriptorReadCallback(gatt, descriptor, value, status); + } + + @Override + public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + super.onDescriptorWrite(gatt, descriptor, status); + onDescriptorWriteCallback(gatt, descriptor, status); + } + + @Override + public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { + super.onMtuChanged(gatt, mtu, status); + onMtuChangedCallback(gatt, mtu, status); + } + + @Override + public void onPhyRead(BluetoothGatt gatt, int txPhy, int rxPhy, int status) { + super.onPhyRead(gatt, txPhy, rxPhy, status); + onPhyReadCallback(gatt, txPhy, rxPhy, status); + } + + @Override + public void onPhyUpdate(BluetoothGatt gatt, int txPhy, int rxPhy, int status) { + super.onPhyUpdate(gatt, txPhy, rxPhy, status); + onPhyUpdateCallback(gatt, txPhy, rxPhy, status); + } + + @Override + public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { + super.onReadRemoteRssi(gatt, rssi, status); + onReadRemoteRssiCallback(gatt, rssi, status); + } + + @Override + public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { + super.onReliableWriteCompleted(gatt, status); + onReliableWriteCompletedCallback(gatt, status); + } + + @Override + public void onServiceChanged(BluetoothGatt gatt) { + super.onServiceChanged(gatt); + onServiceChangedCallback(gatt); + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + super.onServicesDiscovered(gatt, status); + onServicesDiscoveredCallback(gatt, status); + } + + private native void onCharacteristicChangedCallback(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value); + + private native void onCharacteristicReadCallback(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value, int status); + + private native void onCharacteristicWriteCallback(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status); + + private native void onConnectionStateChangeCallback(BluetoothGatt gatt, int status, int newState); + + private native void onDescriptorReadCallback(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, byte[] value, int status); + + private native void onDescriptorWriteCallback(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status); + + private native void onMtuChangedCallback(BluetoothGatt gatt, int mtu, int status); + + private native void onPhyReadCallback(BluetoothGatt gatt, int txPhy, int rxPhy, int status); + + private native void onPhyUpdateCallback(BluetoothGatt gatt, int txPhy, int rxPhy, int status); + + private native void onReadRemoteRssiCallback(BluetoothGatt gatt, int rssi, int status); + + private native void onReliableWriteCompletedCallback(BluetoothGatt gatt, int status); + + private native void onServiceChangedCallback(BluetoothGatt gatt); + + private native void onServicesDiscoveredCallback(BluetoothGatt gatt, int status); +} + diff --git a/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/src/main/java/org/simpleble/android/bridge/ScanCallback.java b/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/src/main/java/org/simpleble/android/bridge/ScanCallback.java new file mode 100644 index 000000000..8db9817bd --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/backends/android/simpleble-bridge/src/main/java/org/simpleble/android/bridge/ScanCallback.java @@ -0,0 +1,36 @@ +package org.simpleble.android.bridge; + +import android.bluetooth.le.ScanResult; + +import java.util.List; +import android.util.Log; + +public class ScanCallback extends android.bluetooth.le.ScanCallback { + + public ScanCallback() {} + + @Override + public void onScanResult(int callbackType, ScanResult result) { + super.onScanResult(callbackType, result); + onScanResultCallback(callbackType, result); + } + + @Override + public void onBatchScanResults(List results) { + super.onBatchScanResults(results); + onBatchScanResultsCallback(results); + } + + @Override + public void onScanFailed(int errorCode) { + super.onScanFailed(errorCode); + onScanFailedCallback(errorCode); + } + + private native void onScanResultCallback(int callbackType, android.bluetooth.le.ScanResult result); + + private native void onScanFailedCallback(int errorCode); + + private native void onBatchScanResultsCallback(List results); + +} diff --git a/third_party/SimpleBLE/simpleble/src/backends/linux/AdapterBase.cpp b/third_party/SimpleBLE/simpleble/src/backends/linux/AdapterBase.cpp index 3b9bf32aa..cba3035bc 100644 --- a/third_party/SimpleBLE/simpleble/src/backends/linux/AdapterBase.cpp +++ b/third_party/SimpleBLE/simpleble/src/backends/linux/AdapterBase.cpp @@ -71,6 +71,9 @@ void AdapterBase::scan_start() { // Start scanning and notify the user. adapter_->discovery_start(); + + // TODO: Does a discovery filter need to be set? + SAFE_CALLBACK_CALL(this->callback_on_scan_start_); is_scanning_ = true; } diff --git a/third_party/SimpleBLE/simpleble/src/backends/linux/PeripheralBase.cpp b/third_party/SimpleBLE/simpleble/src/backends/linux/PeripheralBase.cpp index d8f511f16..e6917e6be 100644 --- a/third_party/SimpleBLE/simpleble/src/backends/linux/PeripheralBase.cpp +++ b/third_party/SimpleBLE/simpleble/src/backends/linux/PeripheralBase.cpp @@ -171,22 +171,14 @@ std::vector PeripheralBase::services() { std::vector PeripheralBase::advertised_services() { std::vector service_list; - for (auto& [service_uuid, value_array] : device_->service_data()) { - service_list.push_back( - ServiceBuilder(service_uuid, ByteArray((const char*)value_array.data(), value_array.size()))); + for (auto& service_uuid : device_->uuids()) { + service_list.push_back(ServiceBuilder(service_uuid)); } return service_list; } -std::map PeripheralBase::manufacturer_data() { - std::map manufacturer_data; - for (auto& [manufacturer_id, value_array] : device_->manufacturer_data()) { - manufacturer_data[manufacturer_id] = ByteArray((const char*)value_array.data(), value_array.size()); - } - - return manufacturer_data; -} +std::map PeripheralBase::manufacturer_data() { return device_->manufacturer_data(); } ByteArray PeripheralBase::read(BluetoothUUID const& service, BluetoothUUID const& characteristic) { // Check if the user is attempting to read the battery service/characteristic and if so, diff --git a/third_party/SimpleBLE/simpleble/src/backends/macos/PeripheralBase.mm b/third_party/SimpleBLE/simpleble/src/backends/macos/PeripheralBase.mm index 5577dc0d1..1c9301ea5 100644 --- a/third_party/SimpleBLE/simpleble/src/backends/macos/PeripheralBase.mm +++ b/third_party/SimpleBLE/simpleble/src/backends/macos/PeripheralBase.mm @@ -126,22 +126,22 @@ return [internal read:service_uuid characteristic_uuid:characteristic_uuid]; } -void PeripheralBase::write_request(BluetoothUUID const& service, BluetoothUUID const& characteristic, ByteArray const& data) { +void PeripheralBase::write_request(BluetoothUUID const& service, BluetoothUUID const& characteristic, ByteArray const& byte_array) { PeripheralBaseMacOS* internal = (__bridge PeripheralBaseMacOS*)opaque_internal_; NSString* service_uuid = [NSString stringWithCString:service.c_str() encoding:NSString.defaultCStringEncoding]; NSString* characteristic_uuid = [NSString stringWithCString:characteristic.c_str() encoding:NSString.defaultCStringEncoding]; - NSData* payload = [NSData dataWithBytes:(void*)data.c_str() length:data.size()]; + NSData* payload = [NSData dataWithBytes:(void*)byte_array.data() length:byte_array.size()]; [internal writeRequest:service_uuid characteristic_uuid:characteristic_uuid payload:payload]; } -void PeripheralBase::write_command(BluetoothUUID const& service, BluetoothUUID const& characteristic, ByteArray const& data) { +void PeripheralBase::write_command(BluetoothUUID const& service, BluetoothUUID const& characteristic, ByteArray const& byte_array) { PeripheralBaseMacOS* internal = (__bridge PeripheralBaseMacOS*)opaque_internal_; NSString* service_uuid = [NSString stringWithCString:service.c_str() encoding:NSString.defaultCStringEncoding]; NSString* characteristic_uuid = [NSString stringWithCString:characteristic.c_str() encoding:NSString.defaultCStringEncoding]; - NSData* payload = [NSData dataWithBytes:(void*)data.c_str() length:data.size()]; + NSData* payload = [NSData dataWithBytes:(void*)byte_array.data() length:byte_array.size()]; [internal writeCommand:service_uuid characteristic_uuid:characteristic_uuid payload:payload]; } @@ -183,13 +183,13 @@ } void PeripheralBase::write(BluetoothUUID const& service, BluetoothUUID const& characteristic, BluetoothUUID const& descriptor, - ByteArray const& data) { + ByteArray const& byte_array) { PeripheralBaseMacOS* internal = (__bridge PeripheralBaseMacOS*)opaque_internal_; NSString* service_uuid = [NSString stringWithCString:service.c_str() encoding:NSString.defaultCStringEncoding]; NSString* characteristic_uuid = [NSString stringWithCString:characteristic.c_str() encoding:NSString.defaultCStringEncoding]; NSString* descriptor_uuid = [NSString stringWithCString:descriptor.c_str() encoding:NSString.defaultCStringEncoding]; - NSData* payload = [NSData dataWithBytes:(void*)data.c_str() length:data.size()]; + NSData* payload = [NSData dataWithBytes:(void*)byte_array.data() length:byte_array.size()]; [internal write:service_uuid characteristic_uuid:characteristic_uuid descriptor_uuid:descriptor_uuid payload:payload]; } diff --git a/third_party/SimpleBLE/simpleble/src/backends/macos/PeripheralBaseMacOS.mm b/third_party/SimpleBLE/simpleble/src/backends/macos/PeripheralBaseMacOS.mm index a4262fd72..1fc66fcfa 100644 --- a/third_party/SimpleBLE/simpleble/src/backends/macos/PeripheralBaseMacOS.mm +++ b/third_party/SimpleBLE/simpleble/src/backends/macos/PeripheralBaseMacOS.mm @@ -213,12 +213,12 @@ - (void)connect { for (CBDescriptor* descriptor in characteristic.descriptors) { @synchronized(self) { [characteristicExtras.descriptorExtras setObject:[[DescriptorExtras alloc] init] - forKey:[[descriptor.UUID UUIDString] lowercaseString]]; + forKey:uuidToString(descriptor.UUID)]; } } @synchronized(self) { - [self.characteristicExtras setObject:characteristicExtras forKey:[[characteristic.UUID UUIDString] lowercaseString]]; + [self.characteristicExtras setObject:characteristicExtras forKey:uuidToString(characteristic.UUID)]; } } } @@ -627,7 +627,7 @@ - (void)peripheral:(CBPeripheral*)peripheral } - (void)peripheral:(CBPeripheral*)peripheral didUpdateValueForCharacteristic:(CBCharacteristic*)characteristic error:(NSError*)error { - CharacteristicExtras* characteristicExtras = [self.characteristicExtras objectForKey:[[characteristic.UUID UUIDString] lowercaseString]]; + CharacteristicExtras* characteristicExtras = [self.characteristicExtras objectForKey:uuidToString(characteristic.UUID)]; if (characteristic.isNotifying) { // If the characteristic is notifying, just save the value and trigger the callback. @@ -651,7 +651,7 @@ - (void)peripheral:(CBPeripheral*)peripheral didUpdateValueForCharacteristic:(CB } - (void)peripheral:(CBPeripheral*)peripheral didWriteValueForCharacteristic:(CBCharacteristic*)characteristic error:(NSError*)error { - CharacteristicExtras* characteristicExtras = [self.characteristicExtras objectForKey:[[characteristic.UUID UUIDString] lowercaseString]]; + CharacteristicExtras* characteristicExtras = [self.characteristicExtras objectForKey:uuidToString(characteristic.UUID)]; BleTask* task = characteristicExtras.task; @synchronized(self) { @@ -663,7 +663,7 @@ - (void)peripheral:(CBPeripheral*)peripheral didWriteValueForCharacteristic:(CBC - (void)peripheral:(CBPeripheral*)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic*)characteristic error:(NSError*)error { - CharacteristicExtras* characteristicExtras = [self.characteristicExtras objectForKey:[[characteristic.UUID UUIDString] lowercaseString]]; + CharacteristicExtras* characteristicExtras = [self.characteristicExtras objectForKey:uuidToString(characteristic.UUID)]; BleTask* task = characteristicExtras.task; @synchronized(self) { @@ -677,8 +677,8 @@ - (void)peripheralIsReadyToSendWriteWithoutResponse:(CBPeripheral*)peripheral { } - (void)peripheral:(CBPeripheral*)peripheral didUpdateValueForDescriptor:(CBDescriptor*)descriptor error:(NSError*)error { - CharacteristicExtras* characteristicExtras = [self.characteristicExtras objectForKey:[[descriptor.characteristic.UUID UUIDString] lowercaseString]]; - DescriptorExtras* descriptorExtras = [characteristicExtras.descriptorExtras objectForKey:[[descriptor.UUID UUIDString] lowercaseString]]; + CharacteristicExtras* characteristicExtras = [self.characteristicExtras objectForKey:uuidToString(descriptor.characteristic.UUID)]; + DescriptorExtras* descriptorExtras = [characteristicExtras.descriptorExtras objectForKey:uuidToString(descriptor.UUID)]; BleTask* task = descriptorExtras.task; @synchronized(self) { @@ -688,8 +688,8 @@ - (void)peripheral:(CBPeripheral*)peripheral didUpdateValueForDescriptor:(CBDesc } - (void)peripheral:(CBPeripheral*)peripheral didWriteValueForDescriptor:(CBDescriptor*)descriptor error:(NSError*)error { - CharacteristicExtras* characteristicExtras = [self.characteristicExtras objectForKey:[[descriptor.characteristic.UUID UUIDString] lowercaseString]]; - DescriptorExtras* descriptorExtras = [characteristicExtras.descriptorExtras objectForKey:[[descriptor.UUID UUIDString] lowercaseString]]; + CharacteristicExtras* characteristicExtras = [self.characteristicExtras objectForKey:uuidToString(descriptor.characteristic.UUID)]; + DescriptorExtras* descriptorExtras = [characteristicExtras.descriptorExtras objectForKey:uuidToString(descriptor.UUID)]; BleTask* task = descriptorExtras.task; @synchronized(self) { diff --git a/third_party/SimpleBLE/simpleble/src/backends/macos/Utils.h b/third_party/SimpleBLE/simpleble/src/backends/macos/Utils.h index 2773cd776..65c49caf2 100644 --- a/third_party/SimpleBLE/simpleble/src/backends/macos/Utils.h +++ b/third_party/SimpleBLE/simpleble/src/backends/macos/Utils.h @@ -6,3 +6,4 @@ #include SimpleBLE::BluetoothUUID uuidToSimpleBLE(CBUUID* uuid); +NSString* uuidToString(CBUUID* uuid); diff --git a/third_party/SimpleBLE/simpleble/src/backends/macos/Utils.mm b/third_party/SimpleBLE/simpleble/src/backends/macos/Utils.mm index 90485f181..d6ceb1825 100644 --- a/third_party/SimpleBLE/simpleble/src/backends/macos/Utils.mm +++ b/third_party/SimpleBLE/simpleble/src/backends/macos/Utils.mm @@ -11,3 +11,13 @@ return uuid_raw; } } + +NSString* uuidToString(CBUUID* uuid) { + NSString* uuidString = [[uuid UUIDString] lowercaseString]; + + if ([uuidString length] == 4) { + return [NSString stringWithFormat:@"0000%@-0000-1000-8000-00805f9b34fb", uuidString]; + } else { + return uuidString; + } +} diff --git a/third_party/SimpleBLE/simpleble/src/backends/plain/PeripheralBase.cpp b/third_party/SimpleBLE/simpleble/src/backends/plain/PeripheralBase.cpp index 479f8886c..69627180f 100644 --- a/third_party/SimpleBLE/simpleble/src/backends/plain/PeripheralBase.cpp +++ b/third_party/SimpleBLE/simpleble/src/backends/plain/PeripheralBase.cpp @@ -70,7 +70,7 @@ std::vector PeripheralBase::services() { std::vector PeripheralBase::advertised_services() { return {}; } -std::map PeripheralBase::manufacturer_data() { return {}; } +std::map PeripheralBase::manufacturer_data() { return {{0x004C, "test"}}; } ByteArray PeripheralBase::read(BluetoothUUID const& service, BluetoothUUID const& characteristic) { return {}; } @@ -81,12 +81,55 @@ void PeripheralBase::write_command(BluetoothUUID const& service, BluetoothUUID c ByteArray const& data) {} void PeripheralBase::notify(BluetoothUUID const& service, BluetoothUUID const& characteristic, - std::function callback) {} + std::function callback) { + if (callback) { + callback_mutex_.lock(); + callbacks_[{service, characteristic}] = std::move(callback); + callback_mutex_.unlock(); + + task_runner_.dispatch( + [this, service, characteristic]() -> std::optional { + std::lock_guard lock(callback_mutex_); + auto it = this->callbacks_.find({service, characteristic}); + + if (it == this->callbacks_.end()) { + return std::nullopt; + } + + it->second("Hello from notify"); + return 1s; + }, + 1s); + } +} void PeripheralBase::indicate(BluetoothUUID const& service, BluetoothUUID const& characteristic, - std::function callback) {} + std::function callback) { + if (callback) { + callback_mutex_.lock(); + callbacks_[{service, characteristic}] = std::move(callback); + callback_mutex_.unlock(); + + task_runner_.dispatch( + [this, service, characteristic]() -> std::optional { + std::lock_guard lock(callback_mutex_); + auto it = this->callbacks_.find({service, characteristic}); + + if (it == this->callbacks_.end()) { + return std::nullopt; + } + + it->second("Hello from notify"); + return 1s; + }, + 1s); + } +} -void PeripheralBase::unsubscribe(BluetoothUUID const& service, BluetoothUUID const& characteristic) {} +void PeripheralBase::unsubscribe(BluetoothUUID const& service, BluetoothUUID const& characteristic) { + std::lock_guard lock(callback_mutex_); + callbacks_.erase({service, characteristic}); +} ByteArray PeripheralBase::read(BluetoothUUID const& service, BluetoothUUID const& characteristic, BluetoothUUID const& descriptor) { diff --git a/third_party/SimpleBLE/simpleble/src/backends/plain/PeripheralBase.h b/third_party/SimpleBLE/simpleble/src/backends/plain/PeripheralBase.h index 434e79c9c..e19a1ad87 100644 --- a/third_party/SimpleBLE/simpleble/src/backends/plain/PeripheralBase.h +++ b/third_party/SimpleBLE/simpleble/src/backends/plain/PeripheralBase.h @@ -5,11 +5,13 @@ #include #include +#include #include #include #include #include +#include namespace SimpleBLE { @@ -59,6 +61,12 @@ class PeripheralBase { kvn::safe_callback callback_on_connected_; kvn::safe_callback callback_on_disconnected_; + + std::mutex callback_mutex_; + std::map, std::function> callbacks_; + + TaskRunner task_runner_; + }; } // namespace SimpleBLE diff --git a/third_party/SimpleBLE/simpleble/src/backends/windows/AdapterBase.cpp b/third_party/SimpleBLE/simpleble/src/backends/windows/AdapterBase.cpp index 7bd3951da..8296914f7 100644 --- a/third_party/SimpleBLE/simpleble/src/backends/windows/AdapterBase.cpp +++ b/third_party/SimpleBLE/simpleble/src/backends/windows/AdapterBase.cpp @@ -99,7 +99,7 @@ AdapterBase::AdapterBase(std::string device_id) (uint8_t)section_data_buffer[5], (uint8_t)section_data_buffer[4], (uint8_t)section_data_buffer[3], (uint8_t)section_data_buffer[2], (uint8_t)section_data_buffer[1], (uint8_t)section_data_buffer[0]); - service_data = section_data_buffer.substr(16); + service_data = section_data_buffer.slice_from(16); } else if (section.DataType() == @@ -107,12 +107,12 @@ AdapterBase::AdapterBase(std::string device_id) service_uuid = fmt::format("{:02x}{:02x}{:02x}{:02x}-0000-1000-8000-00805f9b34fb", (uint8_t)section_data_buffer[3], (uint8_t)section_data_buffer[2], (uint8_t)section_data_buffer[1], (uint8_t)section_data_buffer[0]); - service_data = section_data_buffer.substr(4); + service_data = section_data_buffer.slice_from(4); } else if (section.DataType() == Advertisement::BluetoothLEAdvertisementDataTypes::ServiceData16BitUuids()) { service_uuid = fmt::format("0000{:02x}{:02x}-0000-1000-8000-00805f9b34fb", (uint8_t)section_data_buffer[1], (uint8_t)section_data_buffer[0]); - service_data = section_data_buffer.substr(2); + service_data = section_data_buffer.slice_from(2); } else { continue; } diff --git a/third_party/SimpleBLE/simpleble/src/backends/windows/PeripheralBase.h b/third_party/SimpleBLE/simpleble/src/backends/windows/PeripheralBase.h index 030ce8493..c13bef7db 100644 --- a/third_party/SimpleBLE/simpleble/src/backends/windows/PeripheralBase.h +++ b/third_party/SimpleBLE/simpleble/src/backends/windows/PeripheralBase.h @@ -77,6 +77,8 @@ class PeripheralBase { void set_callback_on_connected(std::function on_connected); void set_callback_on_disconnected(std::function on_disconnected); + // Internal methods not exposed to the user. + void update_advertising_data(advertising_data_t advertising_data); private: diff --git a/third_party/SimpleBLE/simpleble/src/external/TaskRunner.hpp b/third_party/SimpleBLE/simpleble/src/external/TaskRunner.hpp new file mode 100644 index 000000000..e3475122f --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/external/TaskRunner.hpp @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class TaskRunner { + private: + using Clock = std::chrono::steady_clock; + using TimePoint = Clock::time_point; + using Duration = Clock::duration; + using Task = std::function()>; + + struct ScheduledTask { + TimePoint executionTime; + Task task; + + bool operator>(const ScheduledTask& other) const { return executionTime > other.executionTime; } + }; + + std::priority_queue, std::greater> taskQueue; + std::mutex taskQueueMutex; + std::condition_variable taskQueueCV; + bool running = false; + std::thread workerThread; + + public: + TaskRunner() = default; + + ~TaskRunner() { stop(); } + + void start() { + if (!running) { + running = true; + workerThread = std::thread(&TaskRunner::workerLoop, this); + } + } + + void stop() { + if (running) { + { + std::unique_lock lock(taskQueueMutex); + running = false; + taskQueueCV.notify_one(); + } + workerThread.join(); + } + } + + void dispatch(Task task, Duration delay) { + TimePoint executionTime = Clock::now() + delay; + { + std::unique_lock lock(taskQueueMutex); + taskQueue.push({executionTime, std::move(task)}); + taskQueueCV.notify_one(); + } + if (!running) { + start(); + } + } + + private: + void workerLoop() { + while (true) { + std::unique_lock lock(taskQueueMutex); + taskQueueCV.wait(lock, [this] { return !running || !taskQueue.empty(); }); + + if (!running) { + break; + } + + if (taskQueue.empty()) { + continue; + } + + // NOTE: If a new task is added to the queue with a shorter delay than the current top task, the worker thread + // will not wake up until the top task's execution time is reached. This is not ideal, but it's good enough for + // this simple implementation. + + auto now = Clock::now(); + if (taskQueue.top().executionTime > now) { + taskQueueCV.wait_until(lock, taskQueue.top().executionTime); + } + + auto task = std::move(taskQueue.top().task); + taskQueue.pop(); + lock.unlock(); + + auto result = task(); + if (result.has_value()) { + dispatch(std::move(task), *result); + } + } + } +}; \ No newline at end of file diff --git a/third_party/SimpleBLE/simpleble/src/external/ThreadRunner.h b/third_party/SimpleBLE/simpleble/src/external/ThreadRunner.h new file mode 100644 index 000000000..b4f1c6a2b --- /dev/null +++ b/third_party/SimpleBLE/simpleble/src/external/ThreadRunner.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include +#include + +class ThreadRunner { +public: + ThreadRunner() : _stop(false) { + _thread = std::thread(&ThreadRunner::threadFunc, this); + } + + ~ThreadRunner() { + { + std::unique_lock lock(_mutex); + _stop = true; + _cv.notify_one(); + } + _thread.join(); + } + + void enqueue(std::function func) { + { + std::unique_lock lock(_mutex); + _queue.push(std::move(func)); + _cv.notify_one(); + } + } + +private: + void threadFunc() { + while (true) { + std::function func; + { + std::unique_lock lock(_mutex); + _cv.wait(lock, [this] { return _stop || !_queue.empty(); }); + if (_stop && _queue.empty()) { + return; + } + func = std::move(_queue.front()); + _queue.pop(); + } + func(); + } + } + + std::thread _thread; + std::mutex _mutex; + std::condition_variable _cv; + std::queue> _queue; + bool _stop; +}; diff --git a/third_party/SimpleBLE/simpleble/src_c/peripheral.cpp b/third_party/SimpleBLE/simpleble/src_c/peripheral.cpp index dcd8fb1f0..4995f3ba1 100644 --- a/third_party/SimpleBLE/simpleble/src_c/peripheral.cpp +++ b/third_party/SimpleBLE/simpleble/src_c/peripheral.cpp @@ -279,7 +279,7 @@ simpleble_err_t simpleble_peripheral_read(simpleble_peripheral_t handle, simpleb *data_length = read_data.value().size(); *data = static_cast(malloc(*data_length)); - memcpy(*data, read_data.value().c_str(), *data_length); + memcpy(*data, read_data.value().data(), *data_length); return SIMPLEBLE_SUCCESS; } @@ -318,7 +318,7 @@ simpleble_err_t simpleble_peripheral_write_command(simpleble_peripheral_t handle simpleble_err_t simpleble_peripheral_notify( simpleble_peripheral_t handle, simpleble_uuid_t service, simpleble_uuid_t characteristic, - void (*callback)(simpleble_uuid_t, simpleble_uuid_t, const uint8_t*, size_t, void*), void* userdata) { + void (*callback)(simpleble_peripheral_t,simpleble_uuid_t, simpleble_uuid_t, const uint8_t*, size_t, void*), void* userdata) { if (handle == nullptr || callback == nullptr) { return SIMPLEBLE_FAILURE; } @@ -327,7 +327,7 @@ simpleble_err_t simpleble_peripheral_notify( bool success = peripheral->notify(SimpleBLE::BluetoothUUID(service.value), SimpleBLE::BluetoothUUID(characteristic.value), [=](SimpleBLE::ByteArray data) { - callback(service, characteristic, (const uint8_t*)data.data(), data.size(), + callback(handle, service, characteristic, (const uint8_t*)data.data(), data.size(), userdata); }); @@ -336,7 +336,7 @@ simpleble_err_t simpleble_peripheral_notify( simpleble_err_t simpleble_peripheral_indicate( simpleble_peripheral_t handle, simpleble_uuid_t service, simpleble_uuid_t characteristic, - void (*callback)(simpleble_uuid_t, simpleble_uuid_t, const uint8_t*, size_t, void*), void* userdata) { + void (*callback)(simpleble_peripheral_t,simpleble_uuid_t, simpleble_uuid_t, const uint8_t*, size_t, void*), void* userdata) { if (handle == nullptr || callback == nullptr) { return SIMPLEBLE_FAILURE; } @@ -345,7 +345,7 @@ simpleble_err_t simpleble_peripheral_indicate( bool success = peripheral->indicate(SimpleBLE::BluetoothUUID(service.value), SimpleBLE::BluetoothUUID(characteristic.value), [=](SimpleBLE::ByteArray data) { - callback(service, characteristic, (const uint8_t*)data.data(), data.size(), + callback(handle, service, characteristic, (const uint8_t*)data.data(), data.size(), userdata); }); @@ -390,7 +390,7 @@ simpleble_err_t simpleble_peripheral_read_descriptor(simpleble_peripheral_t hand *data_length = read_data.value().size(); *data = static_cast(malloc(*data_length)); - memcpy(*data, read_data.value().c_str(), *data_length); + memcpy(*data, read_data.value().data(), *data_length); return SIMPLEBLE_SUCCESS; } diff --git a/third_party/SimpleBLE/simpleble/src_c/utils.cpp b/third_party/SimpleBLE/simpleble/src_c/utils.cpp index 5198eb523..89c5acbf9 100644 --- a/third_party/SimpleBLE/simpleble/src_c/utils.cpp +++ b/third_party/SimpleBLE/simpleble/src_c/utils.cpp @@ -12,4 +12,4 @@ simpleble_os_t simpleble_get_operating_system() { #endif } -const char* simpleble_get_version() { return "0.6.1"; } +const char* simpleble_get_version() { return SIMPLEBLE_VERSION; } diff --git a/third_party/SimpleBLE/simpleble/test/src/test_bytearray.cpp b/third_party/SimpleBLE/simpleble/test/src/test_bytearray.cpp new file mode 100644 index 000000000..6bcfd8647 --- /dev/null +++ b/third_party/SimpleBLE/simpleble/test/src/test_bytearray.cpp @@ -0,0 +1,246 @@ +#include +#include "external/kvn_bytearray.h" + +using namespace kvn; + +TEST(ByteArrayTest, DefaultConstructor) { + bytearray byteArray; + EXPECT_EQ(byteArray.size(), 0); +} + +TEST(ByteArrayTest, VectorConstructor) { + std::vector vec = {1, 2, 3, 4}; + bytearray byteArray(vec); + EXPECT_EQ(byteArray.size(), 4); + EXPECT_EQ(byteArray[0], 1); + EXPECT_EQ(byteArray[1], 2); + EXPECT_EQ(byteArray[2], 3); + EXPECT_EQ(byteArray[3], 4); +} + +TEST(ByteArrayTest, PointerConstructor) { + uint8_t data[] = {1, 2, 3, 4}; + bytearray byteArray(data, 4); + EXPECT_EQ(byteArray.size(), 4); + EXPECT_EQ(byteArray[0], 1); + EXPECT_EQ(byteArray[1], 2); + EXPECT_EQ(byteArray[2], 3); + EXPECT_EQ(byteArray[3], 4); +} + +TEST(ByteArrayTest, StringConstructor) { + std::string str = "Hello"; + bytearray byteArray(str); + EXPECT_EQ(byteArray.size(), 5); + EXPECT_EQ(byteArray[0], 'H'); + EXPECT_EQ(byteArray[1], 'e'); + EXPECT_EQ(byteArray[2], 'l'); + EXPECT_EQ(byteArray[3], 'l'); + EXPECT_EQ(byteArray[4], 'o'); +} + +TEST(ByteArrayTest, CharArrayConstructor) { + const char str[] = {'H', 'e', 'l', 'l', 'o'}; + bytearray byteArray(str, 5); + EXPECT_EQ(byteArray.size(), 5); + EXPECT_EQ(byteArray[0], 'H'); + EXPECT_EQ(byteArray[1], 'e'); + EXPECT_EQ(byteArray[2], 'l'); + EXPECT_EQ(byteArray[3], 'l'); + EXPECT_EQ(byteArray[4], 'o'); +} + +TEST(ByteArrayTest, CStringConstructor) { + const char* str = "Hello"; + bytearray byteArray(str); + EXPECT_EQ(byteArray.size(), 5); + EXPECT_EQ(byteArray[0], 'H'); + EXPECT_EQ(byteArray[1], 'e'); + EXPECT_EQ(byteArray[2], 'l'); + EXPECT_EQ(byteArray[3], 'l'); + EXPECT_EQ(byteArray[4], 'o'); +} + +TEST(ByteArrayTest, FromHexValid) { + bytearray byteArray = bytearray::fromHex("48656c6C6f"); + EXPECT_EQ(byteArray.size(), 5); + EXPECT_EQ(byteArray[0], 'H'); + EXPECT_EQ(byteArray[1], 'e'); + EXPECT_EQ(byteArray[2], 'l'); + EXPECT_EQ(byteArray[3], 'l'); + EXPECT_EQ(byteArray[4], 'o'); +} + +TEST(ByteArrayTest, FromHexValidWithPrefix) { + bytearray byteArray = bytearray::fromHex("0x48656c6C6f"); + EXPECT_EQ(byteArray.size(), 5); + EXPECT_EQ(byteArray[0], 'H'); + EXPECT_EQ(byteArray[1], 'e'); + EXPECT_EQ(byteArray[2], 'l'); + EXPECT_EQ(byteArray[3], 'l'); + EXPECT_EQ(byteArray[4], 'o'); +} + +TEST(ByteArrayTest, FromHexInvalid) { + EXPECT_THROW(bytearray::fromHex("123"), std::length_error); + EXPECT_THROW(bytearray::fromHex("G123"), std::invalid_argument); +} + +TEST(ByteArrayTest, ToHex) { + bytearray byteArray("Hello"); + EXPECT_EQ(byteArray.toHex(), "48656c6c6f"); + EXPECT_EQ(byteArray.toHex(true), "48 65 6c 6c 6f "); +} + +TEST(ByteArrayTest, StingConversion) { + bytearray byteArray = bytearray::fromHex("48656c6C6f"); + std::string str = static_cast(byteArray); + EXPECT_EQ(str, "Hello"); +} + +TEST(ByteArrayTest, StreamOperator) { + bytearray byteArray("Hello"); + std::ostringstream oss; + oss << byteArray; + EXPECT_EQ(oss.str(), "48 65 6c 6c 6f "); +} + +TEST(ByteArrayTest, DefaultConstructor_Empty) { + bytearray byteArray; + EXPECT_TRUE(byteArray.empty()); +} + +TEST(ByteArrayTest, PushBackIncreasesSize) { + bytearray byteArray; + byteArray.push_back(0x01); + EXPECT_EQ(1, byteArray.size()); + byteArray.push_back(0x02); + EXPECT_EQ(2, byteArray.size()); +} + +TEST(ByteArrayTest, ClearEmptiesArray) { + bytearray byteArray; + byteArray.push_back(0x01); + byteArray.push_back(0x02); + byteArray.clear(); + EXPECT_TRUE(byteArray.empty()); +} + +TEST(ByteArrayTest, IndexOperatorAccessesCorrectElement) { + bytearray byteArray; + byteArray.push_back(0x01); + byteArray.push_back(0x02); + EXPECT_EQ(0x01, byteArray[0]); + EXPECT_EQ(0x02, byteArray[1]); +} + +TEST(ByteArrayTest, DataPointerIsValid) { + bytearray byteArray; + byteArray.push_back(0x01); + byteArray.push_back(0x02); + const uint8_t* data_ptr = byteArray.data(); + ASSERT_NE(nullptr, data_ptr); + EXPECT_EQ(0x01, data_ptr[0]); + EXPECT_EQ(0x02, data_ptr[1]); +} + +TEST(ByteArrayTest, BeginEndIterators) { + bytearray byteArray; + byteArray.push_back(0x01); + byteArray.push_back(0x02); + auto it = byteArray.begin(); + EXPECT_EQ(0x01, *it); + ++it; + EXPECT_EQ(0x02, *it); + ++it; + EXPECT_EQ(byteArray.end(), it); +} + +TEST(ByteArrayTest, ConstIndexOperatorAccessesCorrectElement) { + bytearray byteArray; + byteArray.push_back(0x01); + byteArray.push_back(0x02); + const bytearray constByteArray = byteArray; + EXPECT_EQ(0x01, constByteArray[0]); + EXPECT_EQ(0x02, constByteArray[1]); +} + +TEST(ByteArrayTest, SliceValidRange) { + std::vector vec = {1, 2, 3, 4, 5}; + bytearray byteArray(vec); + bytearray slicedArray = byteArray.slice(1, 3); + + ASSERT_EQ(slicedArray.size(), 2); + EXPECT_EQ(slicedArray[0], 0x02); + EXPECT_EQ(slicedArray[1], 0x03); +} + +TEST(ByteArrayTest, SliceFullRange) { + std::vector vec = {1, 2, 3, 4, 5}; + bytearray byteArray(vec); + bytearray slicedArray = byteArray.slice(0, byteArray.size()); + + ASSERT_EQ(slicedArray.size(), byteArray.size()); + EXPECT_EQ(slicedArray[0], 0x01); + EXPECT_EQ(slicedArray[4], 0x05); +} + +TEST(ByteArrayTest, SliceSingleElement) { + std::vector vec = {1, 2, 3, 4, 5}; + bytearray byteArray(vec); + bytearray slicedArray = byteArray.slice(2, 3); + + ASSERT_EQ(slicedArray.size(), 1); + EXPECT_EQ(slicedArray[0], 0x03); +} + +TEST(ByteArrayTest, SliceOutOfRange) { + std::vector vec = {1, 2, 3, 4, 5}; + bytearray byteArray(vec); + + EXPECT_THROW(byteArray.slice(1, 6), std::out_of_range); + EXPECT_THROW(byteArray.slice(6, 7), std::out_of_range); +} + +TEST(ByteArrayTest, SliceInvalidRange) { + std::vector vec = {1, 2, 3, 4, 5}; + bytearray byteArray(vec); + + EXPECT_THROW(byteArray.slice(3, 2), std::out_of_range); +} + +TEST(ByteArrayTest, SliceFromIndexToEnd) { + std::vector vec = {1, 2, 3, 4, 5}; + bytearray byteArray(vec); + bytearray slicedArray = byteArray.slice_from(2); + + ASSERT_EQ(slicedArray.size(), 3); + EXPECT_EQ(slicedArray[0], 0x03); + EXPECT_EQ(slicedArray[1], 0x04); + EXPECT_EQ(slicedArray[2], 0x05); +} + +TEST(ByteArrayTest, SliceFromBeginningToIndex) { + std::vector vec = {1, 2, 3, 4, 5}; + bytearray byteArray(vec); + bytearray slicedArray = byteArray.slice_to(3); + + ASSERT_EQ(slicedArray.size(), 3); + EXPECT_EQ(slicedArray[0], 0x01); + EXPECT_EQ(slicedArray[1], 0x02); + EXPECT_EQ(slicedArray[2], 0x03); +} + +TEST(ByteArrayTest, SliceFromIndexToEndOutOfRange) { + std::vector vec = {1, 2, 3, 4, 5}; + bytearray byteArray(vec); + + EXPECT_THROW(byteArray.slice_from(6), std::out_of_range); +} + +TEST(ByteArrayTest, SliceFromBeginningToIndexOutOfRange) { + std::vector vec = {1, 2, 3, 4, 5}; + bytearray byteArray(vec); + + EXPECT_THROW(byteArray.slice_to(6), std::out_of_range); +} diff --git a/third_party/SimpleBLE/simplebluez/CMakeLists.txt b/third_party/SimpleBLE/simplebluez/CMakeLists.txt index 7a2847a0d..53cd80d83 100644 --- a/third_party/SimpleBLE/simplebluez/CMakeLists.txt +++ b/third_party/SimpleBLE/simplebluez/CMakeLists.txt @@ -1,5 +1,10 @@ cmake_minimum_required(VERSION 3.21) +# Only allow Linux builds +if(NOT CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") + message(FATAL_ERROR "-- [ERROR] UNSUPPORTED SYSTEM: '${CMAKE_HOST_SYSTEM_NAME}'. SimpleBluez can only be built on Linux.") +endif() + include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/prelude.cmake) project( @@ -16,6 +21,8 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/epilogue.cmake) option(LIBFMT_VENDORIZE "Enable vendorized libfmt" ON) find_package(fmt REQUIRED) +set_target_properties(fmt PROPERTIES EXCLUDE_FROM_ALL TRUE) + find_package(DBus1 REQUIRED) if(NOT DEFINED SIMPLEDBUS_SANITIZE AND DEFINED SIMPLEBLUEZ_SANITIZE) @@ -33,7 +40,8 @@ endif() set(SIMPLEBLUEZ_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/include - ${CMAKE_CURRENT_SOURCE_DIR}/../simpledbus/include) + ${CMAKE_CURRENT_SOURCE_DIR}/../simpledbus/include + ${CMAKE_CURRENT_SOURCE_DIR}/../external/include) set(SIMPLEBLUEZ_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/ProxyOrg.cpp @@ -86,12 +94,17 @@ set_target_properties( list(APPEND PRIVATE_COMPILE_DEFINITIONS SIMPLEDBUS_LOG_LEVEL=${SIMPLEDBUS_LOG_LEVEL}) list(APPEND PRIVATE_COMPILE_DEFINITIONS SIMPLEBLUEZ_LOG_LEVEL=${SIMPLEBLUEZ_LOG_LEVEL}) +if(SIMPLEBLUEZ_USE_SESSION_DBUS) + list(APPEND PRIVATE_COMPILE_DEFINITIONS SIMPLEBLUEZ_USE_SESSION_DBUS) +endif() + target_link_libraries(simplebluez PUBLIC ${DBus1_LIBRARIES}) target_link_libraries(simplebluez PRIVATE $) target_include_directories(simplebluez PRIVATE ${SIMPLEBLUEZ_INCLUDE}) target_include_directories(simplebluez INTERFACE $ + $ $ $ $ @@ -126,6 +139,10 @@ install( DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/include/simplebluez/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/simplebluez) +install( + DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/../external/include/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/simplebluez) + install( DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../simpledbus/include/simpledbus/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/simpledbus) diff --git a/third_party/SimpleBLE/simplebluez/include/simplebluez/Device.h b/third_party/SimpleBLE/simplebluez/include/simplebluez/Device.h index 3cb9e3bf2..6f640f497 100644 --- a/third_party/SimpleBLE/simplebluez/include/simplebluez/Device.h +++ b/third_party/SimpleBLE/simplebluez/include/simplebluez/Device.h @@ -20,6 +20,7 @@ class Device : public SimpleDBus::Proxy { // ----- PROPERTIES ----- std::vector> services(); + std::vector uuids(); std::string address(); std::string address_type(); @@ -28,8 +29,8 @@ class Device : public SimpleDBus::Proxy { int16_t rssi(); int16_t tx_power(); - std::map> manufacturer_data(); - std::map> service_data(); + std::map manufacturer_data(); + std::map service_data(); bool paired(); bool connected(); diff --git a/third_party/SimpleBLE/simplebluez/include/simplebluez/Types.h b/third_party/SimpleBLE/simplebluez/include/simplebluez/Types.h index 346600291..acd1400a4 100644 --- a/third_party/SimpleBLE/simplebluez/include/simplebluez/Types.h +++ b/third_party/SimpleBLE/simplebluez/include/simplebluez/Types.h @@ -1,9 +1,9 @@ #pragma once -#include +#include "external/kvn_bytearray.h" namespace SimpleBluez { -typedef std::string ByteArray; +using ByteArray = kvn::bytearray; } diff --git a/third_party/SimpleBLE/simplebluez/include/simplebluez/interfaces/Device1.h b/third_party/SimpleBLE/simplebluez/include/simplebluez/interfaces/Device1.h index 1192536c6..8f9310a83 100644 --- a/third_party/SimpleBLE/simplebluez/include/simplebluez/interfaces/Device1.h +++ b/third_party/SimpleBLE/simplebluez/include/simplebluez/interfaces/Device1.h @@ -5,6 +5,8 @@ #include +#include "simplebluez/Types.h" + namespace SimpleBluez { class Device1 : public SimpleDBus::Interface { @@ -26,8 +28,9 @@ class Device1 : public SimpleDBus::Interface { std::string AddressType(); std::string Alias(); std::string Name(); - std::map> ManufacturerData(bool refresh = true); - std::map> ServiceData(bool refresh = true); + std::vector UUIDs(); + std::map ManufacturerData(bool refresh = true); + std::map ServiceData(bool refresh = true); bool Paired(bool refresh = true); bool Connected(bool refresh = true); bool ServicesResolved(bool refresh = true); @@ -47,8 +50,8 @@ class Device1 : public SimpleDBus::Interface { std::string _address_type; bool _connected; bool _services_resolved; - std::map> _manufacturer_data; - std::map> _service_data; + std::map _manufacturer_data; + std::map _service_data; }; } // namespace SimpleBluez diff --git a/third_party/SimpleBLE/simplebluez/src/Device.cpp b/third_party/SimpleBLE/simplebluez/src/Device.cpp index e03957c55..ce0f40cfe 100644 --- a/third_party/SimpleBLE/simplebluez/src/Device.cpp +++ b/third_party/SimpleBLE/simplebluez/src/Device.cpp @@ -2,6 +2,8 @@ #include #include +#include + using namespace SimpleBluez; Device::Device(std::shared_ptr conn, const std::string& bus_name, const std::string& path) @@ -10,8 +12,14 @@ Device::Device(std::shared_ptr conn, const std::string& Device::~Device() {} std::shared_ptr Device::path_create(const std::string& path) { - auto child = std::make_shared(_conn, _bus_name, path); - return std::static_pointer_cast(child); + const std::string next_child = SimpleDBus::Path::next_child_strip(_path, path); + + if (next_child.find("service") == 0) { + auto child = std::make_shared(_conn, _bus_name, path); + return std::static_pointer_cast(child); + } else { + return std::make_shared(_conn, _bus_name, path); + } } std::shared_ptr Device::interfaces_create(const std::string& interface_name) { @@ -33,7 +41,9 @@ std::shared_ptr Device::battery1() { return std::dynamic_pointer_cast(interface_get("org.bluez.Battery1")); } -std::vector> Device::services() { return children_casted(); } +std::vector> Device::services() { + return children_casted_with_prefix("service"); +} std::shared_ptr Device::get_service(const std::string& uuid) { auto services_all = services(); @@ -73,9 +83,11 @@ int16_t Device::rssi() { return device1()->RSSI(); } int16_t Device::tx_power() { return device1()->TxPower(); } -std::map> Device::manufacturer_data() { return device1()->ManufacturerData(); } +std::vector Device::uuids() { return device1()->UUIDs(); } + +std::map Device::manufacturer_data() { return device1()->ManufacturerData(); } -std::map> Device::service_data() { return device1()->ServiceData(); } +std::map Device::service_data() { return device1()->ServiceData(); } bool Device::paired() { return device1()->Paired(); } diff --git a/third_party/SimpleBLE/simplebluez/src/interfaces/Device1.cpp b/third_party/SimpleBLE/simplebluez/src/interfaces/Device1.cpp index 5f54f845a..0a5033b2f 100644 --- a/third_party/SimpleBLE/simplebluez/src/interfaces/Device1.cpp +++ b/third_party/SimpleBLE/simplebluez/src/interfaces/Device1.cpp @@ -62,7 +62,18 @@ std::string Device1::Name() { return _properties["Name"].get_string(); } -std::map> Device1::ManufacturerData(bool refresh) { +std::vector Device1::UUIDs() { + std::scoped_lock lock(_property_update_mutex); + + std::vector uuids; + for (SimpleDBus::Holder& uuid : _properties["UUIDs"].get_array()) { + uuids.push_back(uuid.get_string()); + } + + return uuids; +} + +std::map Device1::ManufacturerData(bool refresh) { if (refresh) { property_refresh("ManufacturerData"); } @@ -72,7 +83,7 @@ std::map> Device1::ManufacturerData(bool refresh) return _manufacturer_data; } -std::map> Device1::ServiceData(bool refresh) { +std::map Device1::ServiceData(bool refresh) { if (refresh) { property_refresh("ServiceData"); } @@ -125,7 +136,7 @@ void Device1::property_changed(std::string option_name) { std::map manuf_data = _properties["ManufacturerData"].get_dict_uint16(); // Loop through all received keys and store them. for (auto& [key, value_array] : manuf_data) { - std::vector raw_manuf_data; + ByteArray raw_manuf_data; for (auto& elem : value_array.get_array()) { raw_manuf_data.push_back(elem.get_byte()); } @@ -138,7 +149,7 @@ void Device1::property_changed(std::string option_name) { std::map service_data = _properties["ServiceData"].get_dict_string(); // Loop through all received keys and store them. for (auto& [key, value_array] : service_data) { - std::vector raw_service_data; + ByteArray raw_service_data; for (auto& elem : value_array.get_array()) { raw_service_data.push_back(elem.get_byte()); } diff --git a/third_party/SimpleBLE/simpledbus/CMakeLists.txt b/third_party/SimpleBLE/simpledbus/CMakeLists.txt index eb2ebeebd..8cf127a4a 100644 --- a/third_party/SimpleBLE/simpledbus/CMakeLists.txt +++ b/third_party/SimpleBLE/simpledbus/CMakeLists.txt @@ -1,5 +1,10 @@ cmake_minimum_required(VERSION 3.21) +# Only allow Linux builds +if(NOT CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") + message(FATAL_ERROR "-- [ERROR] UNSUPPORTED SYSTEM: '${CMAKE_HOST_SYSTEM_NAME}'. SimpleDBus can only be built on Linux.") +endif() + include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/prelude.cmake) project( @@ -16,6 +21,8 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/epilogue.cmake) option(LIBFMT_VENDORIZE "Enable vendorized libfmt" ON) find_package(fmt REQUIRED) +set_target_properties(fmt PROPERTIES EXCLUDE_FROM_ALL TRUE) + find_package(DBus1 REQUIRED) # Load all variables that would eventually need to be exposed to downstream projects @@ -109,6 +116,7 @@ if(SIMPLEDBUS_TEST) ${CMAKE_CURRENT_SOURCE_DIR}/test/src/test_message.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test/src/test_proxy_interfaces.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test/src/test_proxy_children.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test/src/test_proxy_lifetime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test/src/test_path.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test/src/helpers/PythonRunner.cpp) diff --git a/third_party/SimpleBLE/simpledbus/include/simpledbus/advanced/Proxy.h b/third_party/SimpleBLE/simpledbus/include/simpledbus/advanced/Proxy.h index e9c91e0cf..b0872b033 100644 --- a/third_party/SimpleBLE/simpledbus/include/simpledbus/advanced/Proxy.h +++ b/third_party/SimpleBLE/simpledbus/include/simpledbus/advanced/Proxy.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -63,6 +64,19 @@ class Proxy { return result; } + template + std::vector> children_casted_with_prefix(const std::string& prefix) { + std::vector> result; + std::scoped_lock lock(_child_access_mutex); + for (auto& [path, child] : _children) { + const std::string next_child = SimpleDBus::Path::next_child_strip(_path, path); + if (next_child.find(prefix) == 0) { + result.push_back(std::dynamic_pointer_cast(child)); + } + } + return result; + } + protected: bool _valid; std::string _path; diff --git a/third_party/SimpleBLE/simpledbus/include/simpledbus/base/Holder.h b/third_party/SimpleBLE/simpledbus/include/simpledbus/base/Holder.h index 177c0ea67..83c4d3817 100644 --- a/third_party/SimpleBLE/simpledbus/include/simpledbus/base/Holder.h +++ b/third_party/SimpleBLE/simpledbus/include/simpledbus/base/Holder.h @@ -8,6 +8,28 @@ namespace SimpleDBus { +class ObjectPath { + public: + ObjectPath(const std::string& path) : path(path) {} + ObjectPath(const char* path) : path(path) {} + operator std::string() const { return path; } + bool operator<(const ObjectPath& other) const { return path < other.path; } + + private: + std::string path; +}; + +class Signature { + public: + Signature(const std::string& signature) : signature(signature) {} + Signature(const char* signature) : signature(signature) {} + operator std::string() const { return signature; } + bool operator<(const Signature& other) const { return signature < other.signature; } + + private: + std::string signature; +}; + class Holder; class Holder { @@ -40,6 +62,7 @@ class Holder { std::string represent(); std::string signature(); + // TODO: Deprecate these functions in favor of templated version. static Holder create_boolean(bool value); static Holder create_byte(uint8_t value); static Holder create_int16(int16_t value); @@ -50,12 +73,14 @@ class Holder { static Holder create_uint64(uint64_t value); static Holder create_double(double value); static Holder create_string(const std::string& str); - static Holder create_object_path(const std::string& str); - static Holder create_signature(const std::string& str); + static Holder create_object_path(const ObjectPath& path); + static Holder create_signature(const Signature& signature); static Holder create_array(); static Holder create_dict(); std::any get_contents() const; + + // TODO: Deprecate these functions in favor of templated version. bool get_boolean() const; uint8_t get_byte() const; int16_t get_int16() const; @@ -83,6 +108,16 @@ class Holder { void dict_append(Type key_type, std::any key, Holder value); void array_append(Holder holder); + // Template speciallizations. + template + static Holder create(); + + template + static Holder create(T value); + + template + T get() const; + private: Type _type = NONE; diff --git a/third_party/SimpleBLE/simpledbus/include/simpledbus/base/Path.h b/third_party/SimpleBLE/simpledbus/include/simpledbus/base/Path.h index 549f3ffcb..450ab80f4 100644 --- a/third_party/SimpleBLE/simpledbus/include/simpledbus/base/Path.h +++ b/third_party/SimpleBLE/simpledbus/include/simpledbus/base/Path.h @@ -18,6 +18,7 @@ class Path { static bool is_parent(const std::string& base, const std::string& path); static std::string next_child(const std::string& base, const std::string& path); + static std::string next_child_strip(const std::string& base, const std::string& path); }; } // namespace SimpleDBus diff --git a/third_party/SimpleBLE/simpledbus/src/base/Holder.cpp b/third_party/SimpleBLE/simpledbus/src/base/Holder.cpp index 24af1344a..d2cfc6558 100644 --- a/third_party/SimpleBLE/simpledbus/src/base/Holder.cpp +++ b/third_party/SimpleBLE/simpledbus/src/base/Holder.cpp @@ -416,16 +416,16 @@ Holder Holder::create_string(const std::string& str) { h.holder_string = str; return h; } -Holder Holder::create_object_path(const std::string& str) { +Holder Holder::create_object_path(const ObjectPath& path) { Holder h; h._type = OBJ_PATH; - h.holder_string = str; + h.holder_string = path; return h; } -Holder Holder::create_signature(const std::string& str) { +Holder Holder::create_signature(const Signature& signature) { Holder h; h._type = SIGNATURE; - h.holder_string = str; + h.holder_string = signature; return h; } Holder Holder::create_array() { @@ -441,6 +441,76 @@ Holder Holder::create_dict() { return h; } +template <> +Holder Holder::create(bool value) { + return create_boolean(value); +} + +template <> +Holder Holder::create(uint8_t value) { + return create_byte(value); +} + +template <> +Holder Holder::create(int16_t value) { + return create_int16(value); +} + +template <> +Holder Holder::create(uint16_t value) { + return create_uint16(value); +} + +template <> +Holder Holder::create(int32_t value) { + return create_int32(value); +} + +template <> +Holder Holder::create(uint32_t value) { + return create_uint32(value); +} + +template <> +Holder Holder::create(int64_t value) { + return create_int64(value); +} + +template <> +Holder Holder::create(uint64_t value) { + return create_uint64(value); +} + +template <> +Holder Holder::create(double value) { + return create_double(value); +} + +template <> +Holder Holder::create(const std::string& value) { + return create_string(value); +} + +template <> +Holder Holder::create(const ObjectPath& value) { + return create_object_path(value); +} + +template <> +Holder Holder::create(const Signature& value) { + return create_signature(value); +} + +template <> +Holder Holder::create>() { + return create_array(); +} + +template <> +Holder Holder::create>() { + return create_dict(); +} + std::any Holder::get_contents() const { // Only return the contents for simple types switch (_type) { @@ -519,6 +589,123 @@ std::map Holder::get_dict_object_path() const { return _get std::map Holder::get_dict_signature() const { return _get_dict(SIGNATURE); } +template <> +bool Holder::get() const { + return get_boolean(); +} + +template <> +uint8_t Holder::get() const { + return get_byte(); +} + +template <> +int16_t Holder::get() const { + return get_int16(); +} + +template <> +uint16_t Holder::get() const { + return get_uint16(); +} + +template <> +int32_t Holder::get() const { + return get_int32(); +} + +template <> +uint32_t Holder::get() const { + return get_uint32(); +} + +template <> +int64_t Holder::get() const { + return get_int64(); +} + +template <> +uint64_t Holder::get() const { + return get_uint64(); +} + +template <> +double Holder::get() const { + return get_double(); +} + +template <> +std::string Holder::get() const { + return get_string(); +} + +template <> +std::vector Holder::get() const { + return get_array(); +} + +template <> +std::map Holder::get() const { + return get_dict_uint8(); +} + +template <> +std::map Holder::get() const { + return get_dict_uint16(); +} + +template <> +std::map Holder::get() const { + return get_dict_uint32(); +} + +template <> +std::map Holder::get() const { + return get_dict_uint64(); +} + +template <> +std::map Holder::get() const { + return get_dict_int16(); +} + +template <> +std::map Holder::get() const { + return get_dict_int32(); +} + +template <> +std::map Holder::get() const { + return get_dict_int64(); +} + +template <> +std::map Holder::get() const { + return get_dict_string(); +} + +template <> +std::map Holder::get() const { + std::map output; + for (auto& [key_type_internal, key, value] : holder_dict) { + if (key_type_internal == OBJ_PATH) { + output[ObjectPath(std::any_cast(key))] = value; + } + } + return output; +} + +template <> +std::map Holder::get() const { + std::map output; + for (auto& [key_type_internal, key, value] : holder_dict) { + if (key_type_internal == SIGNATURE) { + output[Signature(std::any_cast(key))] = value; + } + } + return output; +} + void Holder::array_append(Holder holder) { holder_array.push_back(holder); } void Holder::dict_append(Type key_type, std::any key, Holder value) { diff --git a/third_party/SimpleBLE/simpledbus/src/base/Path.cpp b/third_party/SimpleBLE/simpledbus/src/base/Path.cpp index 4082fc90c..4e231683e 100644 --- a/third_party/SimpleBLE/simpledbus/src/base/Path.cpp +++ b/third_party/SimpleBLE/simpledbus/src/base/Path.cpp @@ -121,4 +121,9 @@ std::string Path::next_child(const std::string& base, const std::string& path) { return fetch_elements(path, count_elements(base) + 1); } +std::string Path::next_child_strip(const std::string& base, const std::string& path) { + const std::string child = next_child(base, path); + return child.substr(base.length() + 1); +} + } // namespace SimpleDBus diff --git a/third_party/SimpleBLE/simpledroidble/.gitignore b/third_party/SimpleBLE/simpledroidble/.gitignore new file mode 100644 index 000000000..aa724b770 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/third_party/SimpleBLE/simpledroidble/build.gradle.kts b/third_party/SimpleBLE/simpledroidble/build.gradle.kts new file mode 100644 index 000000000..001c1b800 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/build.gradle.kts @@ -0,0 +1,5 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.jetbrainsKotlinAndroid) apply false + alias(libs.plugins.androidLibrary) apply false +} \ No newline at end of file diff --git a/third_party/SimpleBLE/simpledroidble/gradle.properties b/third_party/SimpleBLE/simpledroidble/gradle.properties new file mode 100644 index 000000000..20e2a0152 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/third_party/SimpleBLE/simpledroidble/gradle/libs.versions.toml b/third_party/SimpleBLE/simpledroidble/gradle/libs.versions.toml new file mode 100644 index 000000000..612a7bd0b --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/gradle/libs.versions.toml @@ -0,0 +1,15 @@ +[versions] +agp = "8.3.1" +kotlin = "1.9.23" +coreKtx = "1.12.0" +appcompat = "1.6.1" +material = "1.11.0" + +[libraries] +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } + +[plugins] +jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } \ No newline at end of file diff --git a/third_party/SimpleBLE/simpledroidble/gradle/wrapper/gradle-wrapper.jar b/third_party/SimpleBLE/simpledroidble/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/third_party/SimpleBLE/simpledroidble/gradle/wrapper/gradle-wrapper.properties b/third_party/SimpleBLE/simpledroidble/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..48f2233cc --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Apr 02 22:24:45 PDT 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/third_party/SimpleBLE/simpledroidble/gradlew b/third_party/SimpleBLE/simpledroidble/gradlew new file mode 100644 index 000000000..4f906e0c8 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/third_party/SimpleBLE/simpledroidble/gradlew.bat b/third_party/SimpleBLE/simpledroidble/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/third_party/SimpleBLE/simpledroidble/settings.gradle.kts b/third_party/SimpleBLE/simpledroidble/settings.gradle.kts new file mode 100644 index 000000000..eb11b9c51 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/settings.gradle.kts @@ -0,0 +1,29 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "SimpleDroidBle" + +includeBuild("../simpleble/src/backends/android/simpleble-bridge") { + dependencySubstitution { + substitute(module("org.simpleble.android.bridge:simpleble-bridge")).using(project(":")) + } +} +include(":simpledroidble") diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/.gitignore b/third_party/SimpleBLE/simpledroidble/simpledroidble/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/build.gradle.kts b/third_party/SimpleBLE/simpledroidble/simpledroidble/build.gradle.kts new file mode 100644 index 000000000..6b98e4965 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + alias(libs.plugins.androidLibrary) + alias(libs.plugins.jetbrainsKotlinAndroid) +} + +android { + namespace = "org.simpleble.android" + compileSdk = 34 + + defaultConfig { + minSdk = 31 + + consumerProguardFiles("consumer-rules.pro") + externalNativeBuild { + cmake { + cppFlags("") + } + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + externalNativeBuild { + cmake { + path("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + + //noinspection UseTomlInstead + implementation("org.simpleble.android.bridge:simpleble-bridge") +} \ No newline at end of file diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/consumer-rules.pro b/third_party/SimpleBLE/simpledroidble/simpledroidble/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/proguard-rules.pro b/third_party/SimpleBLE/simpledroidble/simpledroidble/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/AndroidManifest.xml b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/AndroidManifest.xml new file mode 100644 index 000000000..eae847973 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/CMakeLists.txt b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/CMakeLists.txt new file mode 100644 index 000000000..c5ee0fdc3 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/CMakeLists.txt @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 3.21) + +set(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../..) + +include(${PROJECT_ROOT_DIR}/cmake/prelude.cmake) + +project( + "simpleble-jni" + VERSION ${SIMPLEBLE_VERSION} + LANGUAGES CXX +) + +include(${PROJECT_ROOT_DIR}/cmake/epilogue.cmake) + +set(BUILD_SHARED_LIBS OFF) +add_subdirectory(${PROJECT_ROOT_DIR}/simpleble ${CMAKE_BINARY_DIR}/simpleble) +set(BUILD_SHARED_LIBS ON) + +if(NOT TARGET fmt::fmt-header-only) + option(LIBFMT_VENDORIZE "Enable vendorized libfmt" ON) + find_package(fmt REQUIRED) + + if(TARGET fmt) + set_target_properties(fmt PROPERTIES EXCLUDE_FROM_ALL TRUE) + endif() +endif() + +add_library( + ${CMAKE_PROJECT_NAME} SHARED + simpleble_android.cpp + android_utils.cpp +) + +set_target_properties( + ${CMAKE_PROJECT_NAME} PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO +) + +target_include_directories( + ${CMAKE_PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries(${CMAKE_PROJECT_NAME} + android log simpleble::simpleble fmt::fmt-header-only +) \ No newline at end of file diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/ThreadRunner.h b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/ThreadRunner.h new file mode 100644 index 000000000..3ab402222 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/ThreadRunner.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "android_utils.h" +#include +#include + +class ThreadRunner { +public: + ThreadRunner() : _stop(false), _jvm(nullptr) {} + + ~ThreadRunner() { + { + std::unique_lock lock(_mutex); + _stop = true; + _cv.notify_one(); + } + if (_thread && _thread->joinable()) { + _thread->join(); + } + } + + void set_jvm(JavaVM *jvm) { + _jvm = jvm; + } + + void enqueue(std::function func) { + { + std::unique_lock lock(_mutex); + if (!_thread) { + // Thread is lazily started upon the first task enqueue + _thread.emplace(&ThreadRunner::threadFunc, this); + } + _queue.push(std::move(func)); + } + _cv.notify_one(); + } + +private: + void threadFunc() { + if (!_jvm) { + log_error("No JVM provided"); + return; + } + + // Attach the thread to the JVM + JNIEnv *env; + jint result = _jvm->AttachCurrentThread(&env, NULL); + if (result != JNI_OK) { + log_error("Failed to attach thread"); + return; + } + + // Run the thread loop + while (true) { + std::function func; + + // Retrieve and execute the next task + { + std::unique_lock lock(_mutex); + _cv.wait(lock, [this] { return _stop || !_queue.empty(); }); + if (_stop && _queue.empty()) { + break; + } + func = std::move(_queue.front()); + _queue.pop(); + } + + try { + func(); + } catch (const std::exception &e) { + log_error(fmt::format("Exception in thread: {}", e.what())); + } + } + + // Detach the thread from the JVM + _jvm->DetachCurrentThread(); + } + + JavaVM *_jvm; + std::optional _thread; + std::mutex _mutex; + std::condition_variable _cv; + std::queue> _queue; + bool _stop; +}; diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/android_utils.cpp b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/android_utils.cpp new file mode 100644 index 000000000..8982f2530 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/android_utils.cpp @@ -0,0 +1,64 @@ +#include "android_utils.h" + +#include +#include + +void log_error(const std::string& msg) { + __android_log_write(ANDROID_LOG_ERROR, "SimpleBLE", msg.c_str()); +} +void log_info(const std::string& msg) { + __android_log_write(ANDROID_LOG_INFO, "SimpleBLE", msg.c_str()); +} +void log_debug(const std::string& msg) { + __android_log_write(ANDROID_LOG_DEBUG, "SimpleBLE", msg.c_str()); +} + +jstring to_jstring(JNIEnv* env, const std::string& str){ + return env->NewStringUTF(str.c_str()); +} + +std::string from_jstring(JNIEnv* env, jstring str){ + const char* c_str = env->GetStringUTFChars(str, nullptr); + std::string result(c_str); + env->ReleaseStringUTFChars(str, c_str); + return result; +} + +jbyteArray to_jbyteArray(JNIEnv* env, const std::string& data) { + jbyteArray result = env->NewByteArray(data.size()); + env->SetByteArrayRegion(result, 0, data.size(), reinterpret_cast(data.data())); + + jsize length = env->GetArrayLength(result); + jbyte* bytes = env->GetByteArrayElements(result, NULL); + + std::string arrayOut = "Array: "; + for (jsize i = 0; i < length; i++) { + arrayOut += fmt::format("{:02x} ", bytes[i]); + } + log_debug(arrayOut); + + env->ReleaseByteArrayElements(result, bytes, JNI_ABORT); + + + return result; +} + +jobject jarraylist_new(JNIEnv* env) { + jclass arrayListClass = env->FindClass("java/util/ArrayList"); + jmethodID arrayListConstructor = env->GetMethodID(arrayListClass, "", "()V"); + jobject arrayList = env->NewObject(arrayListClass, arrayListConstructor); + return arrayList; +} + +void jarraylist_add(JNIEnv* env, jobject arrayList, jobject element) { + jclass arrayListClass = env->GetObjectClass(arrayList); + jmethodID arrayListAdd = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z"); + env->CallBooleanMethod(arrayList, arrayListAdd, element); +} + +void throw_exception(JNIEnv* env, const std::string& msg) { + log_error(fmt::format("Throwing exception: {}", msg)); + + jclass Exception = env->FindClass("java/lang/Exception"); + env->ThrowNew(Exception, msg.c_str()); +} \ No newline at end of file diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/android_utils.h b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/android_utils.h new file mode 100644 index 000000000..d8565bda2 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/android_utils.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +void log_error(const std::string& msg); +void log_info(const std::string& msg); +void log_debug(const std::string& msg); + +jstring to_jstring(JNIEnv* env, const std::string& str); +std::string from_jstring(JNIEnv* env, jstring str); + +jbyteArray to_jbyteArray(JNIEnv* env, const std::string& data); + +jobject jarraylist_new(JNIEnv* env); +void jarraylist_add(JNIEnv* env, jobject arrayList, jobject element); + +void throw_exception(JNIEnv* env, const std::string& msg); \ No newline at end of file diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/simpleble_android.cpp b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/simpleble_android.cpp new file mode 100644 index 000000000..f213c4e49 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/cpp/simpleble_android.cpp @@ -0,0 +1,784 @@ +#include +#include + +#include "android_utils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "ThreadRunner.h" + +// TODO: Switch to using regular SimpleBLE classes with try/catch blocks. + +static std::map cached_adapters; +static std::map> cached_adapter_callbacks; + +static std::map> cached_peripherals; +static std::map>> cached_peripheral_callbacks; +static std::map>> cached_peripheral_data_callbacks; +static ThreadRunner threadRunner; +static JavaVM *jvm; + +JNIEnv* get_env() { + JNIEnv *env; + jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); + return env; +} + +JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) { + jvm = vm; + threadRunner.set_jvm(vm); + +// // Find your class. JNI_OnLoad is called from the correct class loader context for this to work. +// jclass c = env->FindClass("com/example/app/package/MyClass"); +// if (c == nullptr) return JNI_ERR; + + SimpleBLE::Logging::Logger::get()->set_callback( + [](SimpleBLE::Logging::Level level, const std::string& module, const std::string& file, uint32_t line, const std::string& function, const std::string& message) { + std::string log_message = fmt::format("[{}] {}:{}:{}: {}", module, file, line, function, message); + log_info(log_message); + } + ); + + return JNI_VERSION_1_6; +} + +extern "C" +JNIEXPORT void JNICALL +Java_org_simpleble_android_Adapter_nativeAdapterRegister(JNIEnv *env, jobject thiz, jlong adapter_id, jobject callback) { + // TODO: IDEA. We could store the callback object whenever the scan starts and then remove it when the scan stops, + // to avoid having extra references lying around. + + // Create a weak global reference to the Java callback object + jweak weakCallbackRef = env->NewWeakGlobalRef(callback); + + // Store the weak reference in the cached_adapter_callbacks map + cached_adapter_callbacks[adapter_id].push_back(weakCallbackRef); + + // Retrieve the adapter from the cached_adapters map + auto adapter = cached_adapters.at(adapter_id); + + // TODO: Remove any invalid objects before adding new ones. + + adapter.set_callback_on_scan_start([adapter_id](){ + threadRunner.enqueue([adapter_id](){ + JNIEnv *env = get_env(); + + // Retrieve the weak references from the cached_adapter_callbacks map + std::vector weakCallbackRefs = cached_adapter_callbacks[adapter_id]; + + // Iterate over the weak references + for (jweak weakCallbackRef : weakCallbackRefs) { + + // Check if the weak reference is still valid + if (env->IsSameObject(weakCallbackRef, nullptr) == JNI_FALSE) { + // Retrieve the strong reference from the weak reference + jobject callbackRef = env->NewLocalRef(weakCallbackRef); + + // Find the Java class and method to invoke + // TODO: We should cache the class and method IDs + jclass callbackClass = env->GetObjectClass(callbackRef); + jmethodID onScanStartMethod = env->GetMethodID(callbackClass, "onScanStart", "()V"); + + // Invoke the Java callback method + env->CallVoidMethod(callbackRef, onScanStartMethod); + + // Delete the local reference + env->DeleteLocalRef(callbackRef); + } + } + }); + }); + + adapter.set_callback_on_scan_stop([adapter_id](){ + threadRunner.enqueue([adapter_id](){ + JNIEnv *env = get_env(); + + // Retrieve the weak references from the cached_adapter_callbacks map + std::vector weakCallbackRefs = cached_adapter_callbacks[adapter_id]; + + // Iterate over the weak references + for (jweak weakCallbackRef : weakCallbackRefs) { + // Check if the weak reference is still valid + if (env->IsSameObject(weakCallbackRef, nullptr) == JNI_FALSE) { + // Retrieve the strong reference from the weak reference + jobject callbackRef = env->NewLocalRef(weakCallbackRef); + + // Find the Java class and method to invoke + // TODO: We should cache the class and method IDs + jclass callbackClass = env->GetObjectClass(callbackRef); + jmethodID onScanStopMethod = env->GetMethodID(callbackClass, "onScanStop", "()V"); + + // Invoke the Java callback method + env->CallVoidMethod(callbackRef, onScanStopMethod); + + // Delete the local reference + env->DeleteLocalRef(callbackRef); + } + } + }); + }); + + adapter.set_callback_on_scan_found([adapter_id](SimpleBLE::Safe::Peripheral peripheral){ + size_t peripheral_hash = std::hash{}(peripheral.address().value_or("UNKNOWN")); + + // Add to the cache if it doesn't exist + if (cached_peripherals[adapter_id].count(peripheral_hash) == 0) { + cached_peripherals[adapter_id].insert({peripheral_hash, peripheral}); + } + + threadRunner.enqueue([adapter_id, peripheral_hash](){ + JNIEnv *env = get_env(); + + // Retrieve the weak references from the cached_adapter_callbacks map + std::vector weakCallbackRefs = cached_adapter_callbacks[adapter_id]; + + // Iterate over the weak references + for (jweak weakCallbackRef : weakCallbackRefs) { + // Check if the weak reference is still valid + if (env->IsSameObject(weakCallbackRef, nullptr) == JNI_FALSE) { + + // Retrieve the strong reference from the weak reference + jobject callbackRef = env->NewLocalRef(weakCallbackRef); + + // Find the Java class and method to invoke + // TODO: We should cache the class and method IDs + jclass callbackClass = env->GetObjectClass(callbackRef); + jmethodID onScanFoundMethod = env->GetMethodID(callbackClass, "onScanFound","(J)V"); + + // Invoke the Java callback method + env->CallVoidMethod(callbackRef, onScanFoundMethod, peripheral_hash); + + // Delete the local reference + env->DeleteLocalRef(callbackRef); + + } + } + }); + }); + + adapter.set_callback_on_scan_updated([adapter_id](SimpleBLE::Safe::Peripheral peripheral){ + size_t peripheral_hash = std::hash{}(peripheral.address().value_or("UNKNOWN")); + + // Add to the cache if it doesn't exist + if (cached_peripherals[adapter_id].count(peripheral_hash) == 0) { + cached_peripherals[adapter_id].insert({peripheral_hash, peripheral}); + } + + threadRunner.enqueue([adapter_id, peripheral_hash](){ + JNIEnv *env = get_env(); + + // Retrieve the weak references from the cached_adapter_callbacks map + std::vector weakCallbackRefs = cached_adapter_callbacks[adapter_id]; + + // Iterate over the weak references + for (jweak weakCallbackRef : weakCallbackRefs) { + // Check if the weak reference is still valid + if (env->IsSameObject(weakCallbackRef, nullptr) == JNI_FALSE) { + + // Retrieve the strong reference from the weak reference + jobject callbackRef = env->NewLocalRef(weakCallbackRef); + + // Find the Java class and method to invoke + // TODO: We should cache the class and method IDs + jclass callbackClass = env->GetObjectClass(callbackRef); + jmethodID onScanFoundMethod = env->GetMethodID(callbackClass, "onScanUpdated", "(J)V"); + + // Invoke the Java callback method + env->CallVoidMethod(callbackRef, onScanFoundMethod, peripheral_hash); + + // Delete the local reference + env->DeleteLocalRef(callbackRef); + + } + } + }); + }); +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_org_simpleble_android_Adapter_00024Companion_nativeIsBluetoothEnabled(JNIEnv *env, jobject thiz) { + return SimpleBLE::Safe::Adapter::bluetooth_enabled().value_or(false); +} + +extern "C" JNIEXPORT jlongArray JNICALL Java_org_simpleble_android_Adapter_nativeGetAdapters(JNIEnv *env, jclass clazz) { + auto adapters = SimpleBLE::Safe::Adapter::get_adapters(); + + // If an error occurred, return an empty list. + if (!adapters.has_value()) return env->NewLongArray(0); + + // Go over the results, cache whatever doesn't exist and return the full list. + jsize j_adapter_index = 0; + jlongArray j_adapter_result = env->NewLongArray(static_cast(adapters.value().size())); + for (auto &adapter: adapters.value()) { + size_t adapter_hash = std::hash{}(adapter.identifier().value_or("UNKNOWN")); + + // Add to the cache if it doesn't exist + if (cached_adapters.count(adapter_hash) == 0) { + cached_adapters.insert({adapter_hash, adapter}); + } + + // Add to the results + jlong j_adapter_hash = adapter_hash; + env->SetLongArrayRegion(j_adapter_result, j_adapter_index, 1, &j_adapter_hash); + j_adapter_index++; + + } + + return j_adapter_result; +} + +extern "C" JNIEXPORT jstring JNICALL Java_org_simpleble_android_Adapter_nativeAdapterIdentifier(JNIEnv *env, jobject thiz, jlong adapter_id) { + auto adapter = cached_adapters.at(adapter_id); + // TODO: Should throw exception in case of failure. + return to_jstring(env, adapter.identifier().value_or("Unknown")); +} + +extern "C" JNIEXPORT jstring JNICALL Java_org_simpleble_android_Adapter_nativeAdapterAddress(JNIEnv *env, jobject thiz, jlong adapter_id) { + auto adapter = cached_adapters.at(adapter_id); + // TODO: Should throw exception in case of failure. + return to_jstring(env, adapter.address().value_or("Unknown")); +} + +extern "C" JNIEXPORT void JNICALL Java_org_simpleble_android_Adapter_nativeAdapterScanStart(JNIEnv *env, jobject thiz, jlong adapter_id) { + auto adapter = cached_adapters.at(adapter_id); + bool success = adapter.scan_start(); + + if (!success) { + throw_exception(env, "Failed to start scan"); + } +} + +extern "C" JNIEXPORT void JNICALL Java_org_simpleble_android_Adapter_nativeAdapterScanStop(JNIEnv *env, jobject thiz, jlong adapter_id) { + auto adapter = cached_adapters.at(adapter_id); + bool success = adapter.scan_stop(); + + if (!success) { + throw_exception(env, "Failed to stop scan"); + } +} + +extern "C" JNIEXPORT void JNICALL Java_org_simpleble_android_Adapter_nativeAdapterScanFor(JNIEnv *env, jobject thiz, jlong adapter_id, jint timeout) { + auto adapter = cached_adapters.at(adapter_id); + bool success = adapter.scan_for(timeout); + + if (!success) { + throw_exception(env, "Failed to scan for"); + } +} + +extern "C" JNIEXPORT jboolean JNICALL Java_org_simpleble_android_Adapter_nativeAdapterScanIsActive(JNIEnv *env, jobject thiz, jlong adapter_id) { + auto adapter = cached_adapters.at(adapter_id); + // TODO: Should throw exception in case of failure. + return adapter.scan_is_active().value_or(false); +} + +extern "C" JNIEXPORT jlongArray JNICALL Java_org_simpleble_android_Adapter_nativeAdapterScanGetResults(JNIEnv *env, jobject thiz, jlong adapter_id) { + auto adapter = cached_adapters.at(adapter_id); + + auto peripherals = adapter.scan_get_results(); + + // If an error occurred, return an empty list. + if (!peripherals.has_value()) return env->NewLongArray(0); + + jsize j_peripheral_index = 0; + jlongArray j_peripheral_result = env->NewLongArray(static_cast(peripherals.value().size())); + for (auto &peripheral: peripherals.value()) { + size_t peripheral_hash = std::hash{}(peripheral.address().value_or("UNKNOWN")); + + // Add to the cache if it doesn't exist + if (cached_peripherals[adapter_id].count(peripheral_hash) == 0) { + cached_peripherals[adapter_id].insert({peripheral_hash, peripheral}); + } + + // Add to the results + jlong j_peripheral_hash = peripheral_hash; + env->SetLongArrayRegion(j_peripheral_result, j_peripheral_index, 1, &j_peripheral_hash); + j_peripheral_index++; + } + + return j_peripheral_result; +} + +// PERIPHERAL + +extern "C" +JNIEXPORT void JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralRegister(JNIEnv *env, jobject thiz, + jlong adapter_id, jlong peripheral_id, jobject callback) { +// TODO: IDEA. We could store the callback object whenever the scan starts and then remove it when the scan stops, + // to avoid having extra references lying around. + + // Create a weak global reference to the Java callback object + jweak weakCallbackRef = env->NewWeakGlobalRef(callback); + + // Store the weak reference in the cached_adapter_callbacks map + cached_peripheral_callbacks[adapter_id][peripheral_id].push_back(weakCallbackRef); + + auto peripheral = cached_peripherals[adapter_id].at(peripheral_id); + + // TODO: Remove any invalid objects before adding new ones. + + peripheral.set_callback_on_connected([adapter_id, peripheral_id](){ + threadRunner.enqueue([adapter_id, peripheral_id](){ + JNIEnv *env = get_env(); + + // Retrieve the weak references from the cached_adapter_callbacks map + std::vector weakCallbackRefs = cached_peripheral_callbacks[adapter_id][peripheral_id]; + + // Iterate over the weak references + for (jweak weakCallbackRef : weakCallbackRefs) { + + // Check if the weak reference is still valid + if (env->IsSameObject(weakCallbackRef, nullptr) == JNI_FALSE) { + // Retrieve the strong reference from the weak reference + jobject callbackRef = env->NewLocalRef(weakCallbackRef); + + // Find the Java class and method to invoke + // TODO: We should cache the class and method IDs + jclass callbackClass = env->GetObjectClass(callbackRef); + jmethodID onConnectedMethod = env->GetMethodID(callbackClass, "onConnected", "()V"); + + // Invoke the Java callback method + env->CallVoidMethod(callbackRef, onConnectedMethod); + + // Delete the local reference + env->DeleteLocalRef(callbackRef); + } + } + }); + }); + + peripheral.set_callback_on_disconnected([adapter_id, peripheral_id](){ + threadRunner.enqueue([adapter_id, peripheral_id](){ + JNIEnv *env = get_env(); + + // Retrieve the weak references from the cached_adapter_callbacks map + std::vector weakCallbackRefs = cached_peripheral_callbacks[adapter_id][peripheral_id]; + + // Iterate over the weak references + for (jweak weakCallbackRef : weakCallbackRefs) { + + // Check if the weak reference is still valid + if (env->IsSameObject(weakCallbackRef, nullptr) == JNI_FALSE) { + // Retrieve the strong reference from the weak reference + jobject callbackRef = env->NewLocalRef(weakCallbackRef); + + // Find the Java class and method to invoke + // TODO: We should cache the class and method IDs + jclass callbackClass = env->GetObjectClass(callbackRef); + jmethodID onConnectedMethod = env->GetMethodID(callbackClass, "onDisconnected", "()V"); + + // Invoke the Java callback method + env->CallVoidMethod(callbackRef, onConnectedMethod); + + // Delete the local reference + env->DeleteLocalRef(callbackRef); + } + } + }); + }); +} + +extern "C" +JNIEXPORT jstring JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralIdentifier(JNIEnv *env, jobject thiz, + jlong adapter_id, + jlong peripheral_id) { + auto& peripheral = cached_peripherals[adapter_id].at(peripheral_id); + return to_jstring(env, peripheral.identifier().value_or("Unknown")); +} + +extern "C" +JNIEXPORT jstring JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralAddress(JNIEnv *env, jobject thiz, + jlong adapter_id, jlong peripheral_id) { + auto& peripheral = cached_peripherals[adapter_id].at(peripheral_id); + return to_jstring(env, peripheral.address().value_or("Unknown")); +} + +extern "C" +JNIEXPORT jint JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralAddressType(JNIEnv *env, jobject thiz, + jlong adapter_id, + jlong peripheral_id) { + auto& peripheral = cached_peripherals[adapter_id].at(peripheral_id); + return peripheral.address_type().value_or(SimpleBLE::BluetoothAddressType::UNSPECIFIED); +} + +extern "C" +JNIEXPORT jint JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralRssi(JNIEnv *env, jobject thiz, + jlong adapter_id, jlong peripheral_id) { + auto& peripheral = cached_peripherals[adapter_id].at(peripheral_id); + return peripheral.rssi().value_or(INT16_MIN); +} + +extern "C" +JNIEXPORT jint JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralTxPower(JNIEnv *env, jobject thiz, + jlong adapter_id, jlong peripheral_id) { + auto& peripheral = cached_peripherals[adapter_id].at(peripheral_id); + return peripheral.tx_power().value_or(INT16_MIN); +} + +extern "C" +JNIEXPORT jint JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralMtu(JNIEnv *env, jobject thiz, + jlong adapter_id, jlong peripheral_id) { + auto& peripheral = cached_peripherals[adapter_id].at(peripheral_id); + return peripheral.mtu().value_or(UINT16_MAX); +} + +extern "C" +JNIEXPORT void JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralConnect(JNIEnv *env, jobject thiz, + jlong adapter_id, jlong peripheral_id) { + auto peripheral = cached_peripherals[adapter_id].at(peripheral_id); + + bool success = peripheral.connect(); + if (!success) { + throw_exception(env, "Failed to connect"); + } +} + +extern "C" +JNIEXPORT void JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralDisconnect(JNIEnv *env, jobject thiz, + jlong adapter_id, + jlong peripheral_id) { + auto& peripheral = cached_peripherals[adapter_id].at(peripheral_id); + peripheral.disconnect(); +} + +extern "C" +JNIEXPORT void JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralNotify(JNIEnv *env, jobject thiz, + jlong adapter_id, jlong peripheral_id, + jstring j_service, + jstring j_characteristic, + jobject callback) { + + std::string service = from_jstring(env, j_service); + std::string characteristic = from_jstring(env, j_characteristic); + std::string service_characteristic = service + "_" + characteristic; + size_t service_characteristic_hash = std::hash{}(service_characteristic); + + jobject callbackRef = env->NewGlobalRef(callback); + // TODO: Check if there is a callback already registered for this service_characteristic_hash + cached_peripheral_data_callbacks[adapter_id][peripheral_id].insert({service_characteristic_hash, callbackRef}); + + auto peripheral = cached_peripherals[adapter_id].at(peripheral_id); + bool success = peripheral.notify(service, characteristic, [adapter_id, peripheral_id, service_characteristic_hash](SimpleBLE::ByteArray payload){ + + std::string payload_contents; + for (int i = 0; i < payload.size(); i++) { + payload_contents += fmt::format("{:02X}", (int)(payload[i])); + } + + log_info("Received payload: " + payload_contents); + + threadRunner.enqueue([adapter_id, peripheral_id, service_characteristic_hash, payload]() { + JNIEnv *env = get_env(); + + // Retrieve the weak references from the cached_adapter_callbacks map + jobject callbackRef = cached_peripheral_data_callbacks[adapter_id][peripheral_id].at(service_characteristic_hash); + jbyteArray j_payload = to_jbyteArray(env, payload); + + // TODO: We should cache the class and method IDs + jclass callbackClass = env->GetObjectClass(callbackRef); + jmethodID onDataReceivedMethod = env->GetMethodID(callbackClass, "onDataReceived", "([B)V"); + + // Invoke the Java callback method + env->CallVoidMethod(callbackRef, onDataReceivedMethod, j_payload); + + }); + }); + + if (!success) { + throw_exception(env, "Failed to notify"); + } +} + + +extern "C" +JNIEXPORT void JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralIndicate(JNIEnv *env, jobject thiz, + jlong adapter_id, jlong peripheral_id, + jstring j_service, + jstring j_characteristic, + jobject callback) { + + std::string service = from_jstring(env, j_service); + std::string characteristic = from_jstring(env, j_characteristic); + std::string service_characteristic = service + "_" + characteristic; + size_t service_characteristic_hash = std::hash{}(service_characteristic); + + jobject callbackRef = env->NewGlobalRef(callback); + // TODO: Check if there is a callback already registered for this service_characteristic_hash + cached_peripheral_data_callbacks[adapter_id][peripheral_id].insert({service_characteristic_hash, callbackRef}); + + auto peripheral = cached_peripherals[adapter_id].at(peripheral_id); + bool success = peripheral.indicate(service, characteristic, [adapter_id, peripheral_id, service_characteristic_hash](SimpleBLE::ByteArray payload){ + + std::string payload_contents; + for (int i = 0; i < payload.size(); i++) { + payload_contents += fmt::format("{:02X}", (int)(payload[i])); + } + + log_info("Received payload: " + payload_contents); + + threadRunner.enqueue([adapter_id, peripheral_id, service_characteristic_hash, payload]() { + JNIEnv *env = get_env(); + + // Retrieve the weak references from the cached_adapter_callbacks map + jobject callbackRef = cached_peripheral_data_callbacks[adapter_id][peripheral_id].at(service_characteristic_hash); + jbyteArray j_payload = to_jbyteArray(env, payload); + + // TODO: We should cache the class and method IDs + jclass callbackClass = env->GetObjectClass(callbackRef); + jmethodID onDataReceivedMethod = env->GetMethodID(callbackClass, "onDataReceived", "([B)V"); + + // Invoke the Java callback method + env->CallVoidMethod(callbackRef, onDataReceivedMethod, j_payload); + + }); + }); + + if (!success) { + throw_exception(env, "Failed to notify"); + } +} +extern "C" +JNIEXPORT void JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralUnsubscribe(JNIEnv *env, jobject thiz, + jlong adapter_id, + jlong peripheral_id, + jstring j_service, + jstring j_characteristic) { + std::string service = from_jstring(env, j_service); + std::string characteristic = from_jstring(env, j_characteristic); + std::string service_characteristic = service + "_" + characteristic; + size_t service_characteristic_hash = std::hash{}(service_characteristic); + + auto peripheral = cached_peripherals[adapter_id].at(peripheral_id); + bool success = peripheral.unsubscribe(service, characteristic); + + if (!success) { + throw_exception(env, "Failed to unsubscribe"); + } + + jobject callbackRef = cached_peripheral_data_callbacks[adapter_id][peripheral_id].at(service_characteristic_hash); + + // TODO: Should some check be done here to see if the callbackRef is still valid? + env->DeleteGlobalRef(callbackRef); + cached_peripheral_data_callbacks[adapter_id][peripheral_id].erase(service_characteristic_hash); +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralIsConnected(JNIEnv *env, jobject thiz, + jlong adapter_id, + jlong instance_id) { + auto& peripheral = cached_peripherals[adapter_id].at(instance_id); + return peripheral.is_connected().value_or(false); +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralIsConnectable(JNIEnv *env, jobject thiz, + jlong adapter_id, + jlong instance_id) { + auto& peripheral = cached_peripherals[adapter_id].at(instance_id); + return peripheral.is_connectable().value_or(false); +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralIsPaired(JNIEnv *env, jobject thiz, + jlong adapter_id, + jlong instance_id) { + auto& peripheral = cached_peripherals[adapter_id].at(instance_id); + return peripheral.is_paired().value_or(false); +} + +extern "C" +JNIEXPORT void JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralUnpair(JNIEnv *env, jobject thiz, + jlong adapter_id, jlong instance_id) { + auto& peripheral = cached_peripherals[adapter_id].at(instance_id); + peripheral.unpair(); +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralServices(JNIEnv* env, jobject thiz, + jlong adapter_id, + jlong peripheral_id) { + auto& peripheral = cached_peripherals[adapter_id].at(peripheral_id); + auto services = peripheral.services().value_or(std::vector{}); + + jclass serviceClass = env->FindClass("org/simpleble/android/Service"); + jclass characteristicClass = env->FindClass("org/simpleble/android/Characteristic"); + jclass descriptorClass = env->FindClass("org/simpleble/android/Descriptor"); + + jmethodID serviceConstructor = env->GetMethodID(serviceClass, "", "(Ljava/lang/String;Ljava/util/List;)V"); + jmethodID characteristicConstructor = env->GetMethodID(characteristicClass, "", "(Ljava/lang/String;Ljava/util/List;ZZZZZ)V"); + jmethodID descriptorConstructor = env->GetMethodID(descriptorClass, "", "(Ljava/lang/String;)V"); + + jobject serviceArray = jarraylist_new(env); + + for (auto service : services) { + jstring serviceUUID = to_jstring(env, service.uuid()); + jobject charList = jarraylist_new(env); + + for (auto characteristic : service.characteristics()) { + jstring charUUID = to_jstring(env, characteristic.uuid()); + jobject descList = jarraylist_new(env); + + for (auto descriptor : characteristic.descriptors()) { + jstring descUUID = to_jstring(env, descriptor.uuid()); + jobject jDescriptor = env->NewObject(descriptorClass, descriptorConstructor, descUUID); + jarraylist_add(env, descList, jDescriptor); + } + + jobject jCharacteristic = env->NewObject(characteristicClass, characteristicConstructor, + charUUID, descList, + characteristic.can_read(), + characteristic.can_write_request(), + characteristic.can_write_command(), + characteristic.can_notify(), + characteristic.can_indicate()); + jarraylist_add(env, charList, jCharacteristic); + } + + jobject jService = env->NewObject(serviceClass, serviceConstructor, serviceUUID, charList); + jarraylist_add(env, serviceArray, jService); + } + + return serviceArray; +} + +// Utility function to create a new HashMap and return it +jobject NewHashMap(JNIEnv* env) { + jclass hashMapClass = env->FindClass("java/util/HashMap"); + if (hashMapClass == nullptr) { + return nullptr; // Class not found + } + jmethodID hashMapConstructor = env->GetMethodID(hashMapClass, "", "()V"); + if (hashMapConstructor == nullptr) { + return nullptr; // Constructor method not found + } + jobject hashMap = env->NewObject(hashMapClass, hashMapConstructor); + return hashMap; +} + +// Utility function to add an entry to a HashMap +void HashMapPut(JNIEnv* env, jobject hashMap, jobject key, jobject value) { + jclass hashMapClass = env->GetObjectClass(hashMap); + if (hashMapClass == nullptr) { + return; // Class not found + } + jmethodID hashMapPut = env->GetMethodID(hashMapClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); + if (hashMapPut == nullptr) { + return; // Method not found + } + env->CallObjectMethod(hashMap, hashMapPut, key, value); +} + + +// Convert a C++ int to a Java Integer +jobject to_jInteger(JNIEnv* env, jint value) { + jclass integerClass = env->FindClass("java/lang/Integer"); + if (!integerClass) return nullptr; // Class not found + + jmethodID integerConstructor = env->GetMethodID(integerClass, "", "(I)V"); + if (!integerConstructor) return nullptr; // Constructor method not found + + jobject integerObject = env->NewObject(integerClass, integerConstructor, value); + return integerObject; +} + +// JNI function implementation +extern "C" +JNIEXPORT jobject JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralManufacturerData(JNIEnv* env, jobject thiz, jlong adapter_id, jlong instance_id) { + auto& peripheral = cached_peripherals[adapter_id].at(instance_id); + auto manufacturer_data = peripheral.manufacturer_data().value(); + + jobject hashMap = NewHashMap(env); + if (!hashMap) return nullptr; // Error creating HashMap + + for (const auto& data : manufacturer_data) { + jobject key = to_jInteger(env, static_cast(data.first)); + jbyteArray value = to_jbyteArray(env, data.second); + + HashMapPut(env, hashMap, key, value); + + env->DeleteLocalRef(key); + env->DeleteLocalRef(value); + } + + return hashMap; +} + +extern "C" +JNIEXPORT jbyteArray JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralRead(JNIEnv *env, jobject thiz, + jlong adapter_id, jlong peripheral_id, + jstring j_service, + jstring j_characteristic) { + std::string service = from_jstring(env, j_service); + std::string characteristic = from_jstring(env, j_characteristic); + + auto peripheral = cached_peripherals[adapter_id].at(peripheral_id); + SimpleBLE::ByteArray result = peripheral.read(service, characteristic).value_or(""); + + return to_jbyteArray(env, result); +} +extern "C" +JNIEXPORT void JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralWriteRequest(JNIEnv *env, jobject thiz, + jlong adapter_id, + jlong instance_id, + jstring service, + jstring characteristic, + jbyteArray data) { + // TODO: implement nativePeripheralWriteRequest() +} +extern "C" +JNIEXPORT void JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralWriteCommand(JNIEnv *env, jobject thiz, + jlong adapter_id, + jlong instance_id, + jstring service, + jstring characteristic, + jbyteArray data) { + // TODO: implement nativePeripheralWriteCommand() +} +extern "C" +JNIEXPORT jbyteArray JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralDescriptorRead(JNIEnv *env, jobject thiz, + jlong adapter_id, + jlong instance_id, + jstring service, + jstring characteristic, + jstring descriptor) { + // TODO: implement nativePeripheralDescriptorRead() +} +extern "C" +JNIEXPORT void JNICALL +Java_org_simpleble_android_Peripheral_nativePeripheralDescriptorWrite(JNIEnv *env, jobject thiz, + jlong adapter_id, + jlong instance_id, + jstring service, + jstring characteristic, + jstring descriptor, + jbyteArray data) { + // TODO: implement nativePeripheralDescriptorWrite() +} diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Adapter.kt b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Adapter.kt new file mode 100644 index 000000000..8318df47b --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Adapter.kt @@ -0,0 +1,162 @@ +package org.simpleble.android + +import android.annotation.SuppressLint +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class Adapter private constructor(newInstanceId: Long) { + private val _onScanStart = MutableSharedFlow() + private val _onScanStop = MutableSharedFlow() + private val _onScanActive = MutableSharedFlow() + private val _onScanUpdated = MutableSharedFlow() + private val _onScanFound = MutableSharedFlow() + + private var instanceId: Long = newInstanceId + + private val callbacks = object : Callback { + override fun onScanStart() { + CoroutineScope(Dispatchers.Main).launch { + _onScanStart.emit(Unit) + } + CoroutineScope(Dispatchers.Main).launch { + _onScanActive.emit(true) + } + } + + override fun onScanStop() { + CoroutineScope(Dispatchers.Main).launch { + _onScanStop.emit(Unit) + } + CoroutineScope(Dispatchers.Main).launch { + _onScanActive.emit(false) + } + } + + override fun onScanUpdated(peripheralId: Long) { + CoroutineScope(Dispatchers.Main).launch { + _onScanUpdated.emit(Peripheral(instanceId, peripheralId)) + } + } + + override fun onScanFound(peripheralId: Long) { + CoroutineScope(Dispatchers.Main).launch { + _onScanFound.emit(Peripheral(instanceId, peripheralId)) + } + } + } + + init { + Log.d("SimpleBLE", "Adapter ${this.hashCode()}.init") + nativeAdapterRegister(instanceId, callbacks) + } + + val identifier: String get() { + return nativeAdapterIdentifier(instanceId) ?: "" + } + + val address: BluetoothAddress get() { + return BluetoothAddress(nativeAdapterAddress(instanceId) ?: "") + } + + fun scanStart() { + nativeAdapterScanStart(instanceId) + } + + fun scanStop() { + nativeAdapterScanStop(instanceId) + } + + suspend fun scanFor(timeoutMs: Int) { + withContext(Dispatchers.IO) { + nativeAdapterScanFor(instanceId, timeoutMs) + } + } + + val scanIsActive: Boolean get() { + return nativeAdapterScanIsActive(instanceId) + } + + fun scanGetResults(): List { + return nativeAdapterScanGetResults(instanceId).map { Peripheral(instanceId, it) } + } + + val onScanStart get() = _onScanStart + + val onScanStop get() = _onScanStop + + val onScanActive get() = _onScanActive + + val onScanUpdated get() = _onScanUpdated + + val onScanFound get() = _onScanFound + + fun getPairedPeripherals(): List { + // TODO: Implement + return emptyList() + } + + companion object { + + @JvmStatic + fun isBluetoothEnabled(): Boolean { + return nativeIsBluetoothEnabled() + } + + @JvmStatic + fun getAdapters(): List { +// if (SimpleDroidBle.permissionsGranted.not()) { +// return emptyList() +// } + + val nativeAdapterIds = nativeGetAdapters() + val adapters = ArrayList() + + for (nativeAdapterId in nativeAdapterIds) { + adapters.add(Adapter(nativeAdapterId)) + } + + return adapters + } + + @JvmStatic + private external fun nativeGetAdapters(): LongArray + + private external fun nativeIsBluetoothEnabled(): Boolean + } + + // ---------------------------------------------------------------------------- + + private external fun nativeAdapterRegister(adapterId: Long, callback: Callback) + + private external fun nativeAdapterIdentifier(adapterId: Long): String? + + private external fun nativeAdapterAddress(adapterId: Long): String? + + private external fun nativeAdapterScanStart(adapterId: Long) + + private external fun nativeAdapterScanStop(adapterId: Long) + + private external fun nativeAdapterScanFor(adapterId: Long, timeout: Int) + + private external fun nativeAdapterScanIsActive(adapterId: Long): Boolean + + private external fun nativeAdapterScanGetResults(adapterId: Long) : LongArray + + // TODO: Implement + private external fun nativeAdapterGetPairedPeripherals(adapterId: Long): LongArray + + // ---------------------------------------------------------------------------- + + private interface Callback { + fun onScanStart() + fun onScanStop() + fun onScanUpdated(peripheralId: Long) + fun onScanFound(peripheralId: Long) + } + +} diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/BluetoothAddress.kt b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/BluetoothAddress.kt new file mode 100644 index 000000000..cd530052f --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/BluetoothAddress.kt @@ -0,0 +1,7 @@ +package org.simpleble.android + +class BluetoothAddress(private val address: String) { + override fun toString(): String { + return address + } +} diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/BluetoothAddressType.kt b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/BluetoothAddressType.kt new file mode 100644 index 000000000..13a2837f7 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/BluetoothAddressType.kt @@ -0,0 +1,21 @@ +package org.simpleble.android + +enum class BluetoothAddressType(val value: Int) { + PUBLIC(0), + RANDOM(1), + UNSPECIFIED(2); + + override fun toString(): String { + return when (this) { + PUBLIC -> "Public" + RANDOM -> "Random" + UNSPECIFIED -> "Unspecified" + } + } + + companion object { + fun fromInt(value: Int): BluetoothAddressType { + return values().find { it.value == value } ?: UNSPECIFIED + } + } +} \ No newline at end of file diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/BluetoothUUID.kt b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/BluetoothUUID.kt new file mode 100644 index 000000000..2058cf9a2 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/BluetoothUUID.kt @@ -0,0 +1,7 @@ +package org.simpleble.android + +class BluetoothUUID(private val uuid: String) { + override fun toString(): String { + return uuid + } +} \ No newline at end of file diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Characteristic.kt b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Characteristic.kt new file mode 100644 index 000000000..31509b424 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Characteristic.kt @@ -0,0 +1,11 @@ +package org.simpleble.android + +class Characteristic( + val uuid: String, + val descriptors: List, + val canRead: Boolean, + val canWriteRequest: Boolean, + val canWriteCommand: Boolean, + val canNotify: Boolean, + val canIndicate: Boolean +) \ No newline at end of file diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Descriptor.kt b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Descriptor.kt new file mode 100644 index 000000000..37b7acc15 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Descriptor.kt @@ -0,0 +1,3 @@ +package org.simpleble.android + +class Descriptor(val uuid: String) \ No newline at end of file diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Peripheral.kt b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Peripheral.kt new file mode 100644 index 000000000..7552b23c8 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Peripheral.kt @@ -0,0 +1,284 @@ +package org.simpleble.android + +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class Peripheral internal constructor(newAdapterId: Long, newInstanceId: Long) { + + private var instanceId: Long = newInstanceId + private var adapterId: Long = newAdapterId + + private val _onConnected = MutableSharedFlow() + private val _onConnectionActive = MutableSharedFlow() + private val _onDisconnected = MutableSharedFlow() + + private val callbacks = object : Callback { + override fun onConnected() { + CoroutineScope(Dispatchers.Main).launch { + _onConnected.emit(Unit) + } + CoroutineScope(Dispatchers.Main).launch { + _onConnectionActive.emit(true) + } + } + + override fun onDisconnected() { + CoroutineScope(Dispatchers.Main).launch { + _onDisconnected.emit(Unit) + } + CoroutineScope(Dispatchers.Main).launch { + _onConnectionActive.emit(false) + } + } + } + + init { + Log.d("SimpleBLE", "Peripheral ${this.hashCode()}.init") + nativePeripheralRegister(adapterId, instanceId, callbacks) + } + + val identifier: String get() { + return nativePeripheralIdentifier(adapterId, instanceId) ?: "" + } + + val address: BluetoothAddress get() { + return BluetoothAddress(nativePeripheralAddress(adapterId, instanceId) ?: "") + } + + val addressType: BluetoothAddressType get() { + return BluetoothAddressType.fromInt(nativePeripheralAddressType(adapterId, instanceId)) + } + + val rssi: Int get() { + return nativePeripheralRssi(adapterId, instanceId) + } + + val txPower: Int get() { + return nativePeripheralTxPower(adapterId, instanceId) + } + + val mtu: Int get() { + return nativePeripheralMtu(adapterId, instanceId) + } + + suspend fun connect() { + withContext(Dispatchers.IO) { + nativePeripheralConnect(adapterId, instanceId) + } + } + + suspend fun disconnect() { + withContext(Dispatchers.IO) { + nativePeripheralDisconnect(adapterId, instanceId) + } + } + + val isConnected: Boolean get() { + return nativePeripheralIsConnected(adapterId, instanceId) + } + + val isConnectable: Boolean get() { + return nativePeripheralIsConnectable(adapterId, instanceId) + } + + val isPaired: Boolean get() { + return nativePeripheralIsPaired(adapterId, instanceId) + } + + fun unpair() { + nativePeripheralUnpair(adapterId, instanceId) + } + + fun services(): List { + return nativePeripheralServices(adapterId, instanceId) + } + + fun manufacturerData(): Map { + return nativePeripheralManufacturerData(adapterId, instanceId) + } + + fun read(service: BluetoothUUID, characteristic: BluetoothUUID): ByteArray { + return nativePeripheralRead(adapterId, instanceId, service.toString(), characteristic.toString()) + } + + fun writeRequest(service: BluetoothUUID, characteristic: BluetoothUUID, data: ByteArray) { + // TODO: Implement + } + + fun writeCommand(service: BluetoothUUID, characteristic: BluetoothUUID, data: ByteArray) { + // TODO: Implement + } + + fun notify( + service: BluetoothUUID, + characteristic: BluetoothUUID + ): MutableSharedFlow { + val payloadFlow = MutableSharedFlow() + val dataCallback = object : DataCallback { + override fun onDataReceived(data: ByteArray) { + CoroutineScope(Dispatchers.Main).launch { + payloadFlow.emit(data) + } + } + } + + nativePeripheralNotify(adapterId, instanceId, service.toString(), characteristic.toString(), dataCallback) + return payloadFlow + } + + fun indicate( + service: BluetoothUUID, + characteristic: BluetoothUUID + ): MutableSharedFlow { + val payloadFlow = MutableSharedFlow() + val dataCallback = object : DataCallback { + override fun onDataReceived(data: ByteArray) { + CoroutineScope(Dispatchers.Main).launch { + payloadFlow.emit(data) + } + } + } + + nativePeripheralIndicate(adapterId, instanceId, service.toString(), characteristic.toString(), dataCallback) + return payloadFlow + } + + fun unsubscribe(service: BluetoothUUID, characteristic: BluetoothUUID) { + nativePeripheralUnsubscribe(adapterId, instanceId, service.toString(), characteristic.toString()) + } + + fun read( + service: BluetoothUUID, + characteristic: BluetoothUUID, + descriptor: BluetoothUUID + ): ByteArray { + // TODO: Implement + return ByteArray(0) + } + + fun write( + service: BluetoothUUID, + characteristic: BluetoothUUID, + descriptor: BluetoothUUID, + data: ByteArray + ) { + // TODO: Implement + } + + val onConnected get() = _onConnected + + val onDisconnected get() = _onDisconnected + + val onConnectionActive get() = _onConnectionActive + + /// ---------------------------------------------------------------------------- + + private external fun nativePeripheralRegister(adapterId: Long, instanceId: Long, callback: Callback) + + private external fun nativePeripheralIdentifier(adapterId: Long, instanceId: Long): String? + + private external fun nativePeripheralAddress(adapterId: Long, instanceId: Long): String? + + private external fun nativePeripheralAddressType(adapterId: Long, instanceId: Long): Int + + private external fun nativePeripheralRssi(adapterId: Long, instanceId: Long): Int + + private external fun nativePeripheralTxPower(adapterId: Long, instanceId: Long): Int + + private external fun nativePeripheralMtu(adapterId: Long, instanceId: Long): Int + + private external fun nativePeripheralConnect(adapterId: Long, instanceId: Long) + + private external fun nativePeripheralDisconnect(adapterId: Long, instanceId: Long) + + private external fun nativePeripheralIsConnected(adapterId: Long, instanceId: Long): Boolean + + private external fun nativePeripheralIsConnectable(adapterId: Long, instanceId: Long): Boolean + + private external fun nativePeripheralIsPaired(adapterId: Long, instanceId: Long): Boolean + + private external fun nativePeripheralUnpair(adapterId: Long, instanceId: Long) + + private external fun nativePeripheralServices(adapterId: Long, instanceId: Long): List + + private external fun nativePeripheralManufacturerData(adapterId: Long, instanceId: Long): Map + + private external fun nativePeripheralRead( + adapterId: Long, + instanceId: Long, + service: String, + characteristic: String + ): ByteArray + + private external fun nativePeripheralWriteRequest( + adapterId: Long, + instanceId: Long, + service: String, + characteristic: String, + data: ByteArray + ) + + private external fun nativePeripheralWriteCommand( + adapterId: Long, + instanceId: Long, + service: String, + characteristic: String, + data: ByteArray + ) + + private external fun nativePeripheralNotify( + adapterId: Long, + instanceId: Long, + service: String, + characteristic: String, + dataCallback: DataCallback + ) + + private external fun nativePeripheralIndicate( + adapterId: Long, + instanceId: Long, + service: String, + characteristic: String, + dataCallback: DataCallback + ) + + private external fun nativePeripheralUnsubscribe( + adapterId: Long, + instanceId: Long, + service: String, + characteristic: String) + + private external fun nativePeripheralDescriptorRead( + adapterId: Long, + instanceId: Long, + service: String, + characteristic: String, + descriptor: String + ): ByteArray + + private external fun nativePeripheralDescriptorWrite( + adapterId: Long, + instanceId: Long, + service: String, + characteristic: String, + descriptor: String, + data: ByteArray + ) + + // ---------------------------------------------------------------------------- + + private interface DataCallback { + fun onDataReceived(data: ByteArray) + } + + + private interface Callback { + fun onConnected() + fun onDisconnected() + } + +} \ No newline at end of file diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Service.kt b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Service.kt new file mode 100644 index 000000000..ce64bd36b --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/Service.kt @@ -0,0 +1,6 @@ +package org.simpleble.android + +class Service( + val uuid: String, + val characteristics: List +) \ No newline at end of file diff --git a/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/SimpleDroidBle.kt b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/SimpleDroidBle.kt new file mode 100644 index 000000000..c69fb96f9 --- /dev/null +++ b/third_party/SimpleBLE/simpledroidble/simpledroidble/src/main/java/org/simpleble/android/SimpleDroidBle.kt @@ -0,0 +1,101 @@ +package org.simpleble.android + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import java.lang.ref.WeakReference + +class SimpleDroidBle { + companion object { + + lateinit var contextReference: WeakReference + private val context: Context + get() { + return contextReference.get() + ?: throw IllegalStateException("PermissionsManager: Permissions requested outside activity!") + } + private var hasPermissionBluetooth: Boolean = false + private var hasPermissionBluetoothAdmin: Boolean = false + private var hasPermissionBluetoothConnect: Boolean = false + private var hasPermissionBluetoothScan: Boolean = false + + val hasPermissions: Boolean + get() { + hasPermissionBluetooth = isPermissionGranted(Manifest.permission.BLUETOOTH) + hasPermissionBluetoothAdmin = isPermissionGranted(Manifest.permission.BLUETOOTH_ADMIN) + hasPermissionBluetoothConnect = isPermissionGranted(Manifest.permission.BLUETOOTH_CONNECT) + hasPermissionBluetoothScan = isPermissionGranted(Manifest.permission.BLUETOOTH_SCAN) + + return hasPermissionBluetooth && + hasPermissionBluetoothAdmin && + hasPermissionBluetoothConnect && + hasPermissionBluetoothScan + } + + init { + System.loadLibrary("simpleble-jni") + } + + fun requestPermissions() { + if (!hasPermissions) { + ActivityCompat.requestPermissions( + context as Activity, + arrayOf( + Manifest.permission.BLUETOOTH, + Manifest.permission.BLUETOOTH_ADMIN, + Manifest.permission.BLUETOOTH_CONNECT, + Manifest.permission.BLUETOOTH_SCAN + ), + 1 + ) + } + } + + fun handleOnRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + if (requestCode != 1) return + + val length = permissions.size + if (length != grantResults.size) { + Log.e("PermissionsManager", "Permissions and grant results are not the same size!") + return + } + + for (i in 0 until length) { + val permission = permissions[i] + val grantResult = grantResults[i] + when (permission) { + Manifest.permission.BLUETOOTH -> { + hasPermissionBluetooth = grantResult == PackageManager.PERMISSION_GRANTED + } + Manifest.permission.BLUETOOTH_ADMIN -> { + hasPermissionBluetoothAdmin = grantResult == PackageManager.PERMISSION_GRANTED + } + Manifest.permission.BLUETOOTH_CONNECT -> { + hasPermissionBluetoothConnect = grantResult == PackageManager.PERMISSION_GRANTED + } + Manifest.permission.BLUETOOTH_SCAN -> { + hasPermissionBluetoothScan = grantResult == PackageManager.PERMISSION_GRANTED + } + } + } + } + + + @JvmStatic + fun getVersion(): String { + return "1.0.0" + } + + private fun isPermissionGranted(permission: String): Boolean { + return ContextCompat.checkSelfPermission( + context, + permission + ) == PackageManager.PERMISSION_GRANTED + } + + } +} \ No newline at end of file diff --git a/third_party/SimpleBLE/simplepyble/CMakeLists.txt b/third_party/SimpleBLE/simplepyble/CMakeLists.txt index 6b93fd5ae..9b12ca584 100644 --- a/third_party/SimpleBLE/simplepyble/CMakeLists.txt +++ b/third_party/SimpleBLE/simplepyble/CMakeLists.txt @@ -8,30 +8,33 @@ set(CMAKE_CXX_EXTENSIONS NO) project(simplepyble) +include(GNUInstallDirs) + include(${CMAKE_CURRENT_LIST_DIR}/../cmake/epilogue.cmake) find_package(pybind11 REQUIRED) add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../simpleble ${CMAKE_BINARY_DIR}/simpleble) pybind11_add_module( - simplepyble + _simplepyble src/main.cpp src/wrap_adapter.cpp src/wrap_peripheral.cpp src/wrap_service.cpp src/wrap_characteristic.cpp src/wrap_descriptor.cpp + src/wrap_types.cpp ) -target_link_libraries(simplepyble PRIVATE simpleble::simpleble) -target_compile_definitions(simplepyble PRIVATE "-DSIMPLEPYBLE_VERSION=\"${SIMPLEPYBLE_VERSION}\"") +target_link_libraries(_simplepyble PRIVATE simpleble::simpleble) +target_compile_definitions(_simplepyble PRIVATE "-DSIMPLEPYBLE_VERSION=\"${SIMPLEPYBLE_VERSION}\"") set_target_properties( - simplepyble PROPERTIES + _simplepyble PROPERTIES CXX_STANDARD 17 ) install( - TARGETS simplepyble - LIBRARY DESTINATION "./" # Library needs to be installed at the provided path on CMAKE_INSTALL_PREFIX + TARGETS _simplepyble + DESTINATION . ) diff --git a/third_party/SimpleBLE/simplepyble/README.rst b/third_party/SimpleBLE/simplepyble/README.rst index e51dffd3d..6437939c1 100644 --- a/third_party/SimpleBLE/simplepyble/README.rst +++ b/third_party/SimpleBLE/simplepyble/README.rst @@ -29,6 +29,8 @@ Windows Linux MacOS iOS Windows 10+ Ubuntu 20.04+ 10.15+ (except 12.0, 12.1 and 12.2) 15.0+ =========== ============= =================================== ===== +**NOTE:** WSL does not support Bluetooth. + Installation ------------ @@ -45,10 +47,18 @@ Pull requests are welcome. For major changes, please open an issue first to disc what you would like to change. License -------- +======= + +Since February 15th 2024, SimpleBLE is now available under the GNU General Public License +version 3 (GPLv3), with the option for a commercial license without the GPLv3 restrictions +available for a fee. + +**More information on pricing and commercial terms of service will be available soon.** -All components within this project that have not been bundled from -external creators, are licensed under the terms of the `MIT Licence`_. +To enquire about a commercial license, please contact us at ``contact at simpleble dot org``. + +Likewise, if you are using SimpleBLE in an open-source project and would like to request +a free commercial license or if you have any other questions, please reach out at ``contact at simpleble dot org``. .. Links @@ -58,8 +68,6 @@ external creators, are licensed under the terms of the `MIT Licence`_. .. _code examples: https://github.com/OpenBluetoothToolbox/SimpleBLE/tree/main/examples/simplepyble -.. _MIT Licence: https://github.com/OpenBluetoothToolbox/SimpleBLE/blob/main/LICENCE.md - .. _Discord: https://discord.gg/N9HqNEcvP3 .. _ReadTheDocs: https://simpleble.readthedocs.io/en/latest/ @@ -69,3 +77,8 @@ external creators, are licensed under the terms of the `MIT Licence`_. .. |PyPI Licence| image:: https://img.shields.io/pypi/l/simplepyble +.. Other projects using SimpleBLE + +.. _BrainFlow: https://github.com/brainflow-dev/brainflow +.. _InsideBlue: https://github.com/eriklins/InsideBlue-BLE-Tool +.. _NodeWebBluetooth: https://github.com/thegecko/webbluetooth diff --git a/third_party/SimpleBLE/simplepyble/cmake_build_extension/__init__.py b/third_party/SimpleBLE/simplepyble/cmake_build_extension/__init__.py deleted file mode 100644 index 58361eebe..000000000 --- a/third_party/SimpleBLE/simplepyble/cmake_build_extension/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -import os -from contextlib import contextmanager -from pathlib import Path - -from . import build_ext_option, sdist_command -from .build_extension import BuildExtension -from .cmake_extension import CMakeExtension -from .sdist_command import GitSdistFolder, GitSdistTree - - -@contextmanager -def build_extension_env(): - """ - Creates a context in which build extensions can be imported. - - It fixes a change of behaviour of Python >= 3.8 in Windows: - https://docs.python.org/3/whatsnew/3.8.html#bpo-36085-whatsnew - - Other related resources: - - - https://stackoverflow.com/a/23805306 - - https://www.mail-archive.com/dev@subversion.apache.org/msg40414.html - - Example: - - .. code-block:: python - - from cmake_build_extension import build_extension_env - - with build_extension_env(): - from . import bindings - """ - - cookies = [] - - # Windows and Python >= 3.8 - if hasattr(os, "add_dll_directory"): - - for path in os.environ.get("PATH", "").split(os.pathsep): - - if path and Path(path).is_absolute() and Path(path).is_dir(): - cookies.append(os.add_dll_directory(path)) - - try: - yield - - finally: - for cookie in cookies: - cookie.close() \ No newline at end of file diff --git a/third_party/SimpleBLE/simplepyble/cmake_build_extension/build_ext_option.py b/third_party/SimpleBLE/simplepyble/cmake_build_extension/build_ext_option.py deleted file mode 100644 index 8ee4c42c9..000000000 --- a/third_party/SimpleBLE/simplepyble/cmake_build_extension/build_ext_option.py +++ /dev/null @@ -1,55 +0,0 @@ -from typing import NamedTuple - -from setuptools.command.build_ext import build_ext - - -class BuildExtOption(NamedTuple): - """ - NamedTuple that stores the metadata of a custom build_ext option. - - Example: - - The following option: - - BuildExtOption(variable="define", short="D", help="New compiler define") - - is displayed as follows: - - $ python setup.py build_ext --help - ... - Options for 'BuildExtension' command: - ... - --define (-D) New compiler define - ... - """ - - variable: str - short: str - help: str = "" - - -def add_new_build_ext_option(option: BuildExtOption, override: bool = True): - """ - Workaround to add an existing option shown in python setup.py build_ext -h. - - Args: - option: The new option to add. - override: Delete the option if it already exists. - """ - - if override: - # Remove from the existing build_ext.user_options the option to override - build_ext.user_options = [ - o for o in build_ext.user_options if o[1] is not option.short - ] - else: - # Just check if the option already exists, and raise if it does - for o in build_ext.user_options: - if o[1] == option.short: - raise ValueError(f"Short option '{o[1]}' already exists") - - # The long variable name must finish with =, here we append it - option = option._replace(variable=f"{option.variable}=") - - # Add the new option - build_ext.user_options.append(tuple(option)) \ No newline at end of file diff --git a/third_party/SimpleBLE/simplepyble/cmake_build_extension/build_extension.py b/third_party/SimpleBLE/simplepyble/cmake_build_extension/build_extension.py deleted file mode 100644 index d5c07e88c..000000000 --- a/third_party/SimpleBLE/simplepyble/cmake_build_extension/build_extension.py +++ /dev/null @@ -1,281 +0,0 @@ -import importlib.util -import os -import platform -import shutil -import subprocess -from pathlib import Path - -from setuptools.command.build_ext import build_ext - -from .build_ext_option import BuildExtOption, add_new_build_ext_option -from .cmake_extension import CMakeExtension - -# These options are listed in `python setup.py build_ext -h` -custom_options = [ - BuildExtOption( - variable="define", - short="D", - help="Create or update CMake cache " "(example: '-DBAR=b;FOO=f')", - ), - BuildExtOption( - variable="component", - short="C", - help="Install only a specific CMake component (example: '-Cbindings')", - ), - BuildExtOption( - variable="no-cmake-extension", - short="K", - help="Disable a CMakeExtension module (examples: '-Kall', '-Kbar', '-Kbar;foo')", - ), -] - -for o in custom_options: - add_new_build_ext_option(option=o, override=True) - - -class BuildExtension(build_ext): - """ - Setuptools build extension handler. - It processes all the extensions listed in the 'ext_modules' entry. - """ - - def initialize_options(self): - - # Initialize base class - build_ext.initialize_options(self) - - # Initialize the '--define' custom option, overriding the pre-existing one. - # Originally, it was aimed to pass C preprocessor definitions, but instead we - # use it to pass custom configuration options to CMake. - self.define = None - - # Initialize the '--component' custom option. - # It overrides the content of the cmake_component option of CMakeExtension. - self.component = None - - def finalize_options(self): - - # Parse the custom CMake options and store them in a new attribute - defines = [] if self.define is None else self.define.split(";") - self.cmake_defines = [f"-D{define}" for define in defines] - - # Call base class - build_ext.finalize_options(self) - - def run(self) -> None: - # Check that CMake is installed - if shutil.which("cmake") is None: - raise RuntimeError("Required command 'cmake' not found") - - # Check that Ninja is installed - if shutil.which("ninja") is None: - raise RuntimeError("Required command 'ninja' not found") - - for ext in self.extensions: - self.build_extension(ext) - - def __get_msvc_arch(self) -> str: - if self.plat_name == "win32": - return "Win32" - elif self.plat_name == "win-amd64": - return "x64" - elif self.plat_name == "win-arm32": - return "ARM" - elif self.plat_name == "win-arm64": - return "ARM64" - else: - raise RuntimeError(f"Unsupported platform: {platform}") - - def build_extension(self, ext: CMakeExtension) -> None: - """ - Build a CMakeExtension object. - - Args: - ext: The CMakeExtension object to build. - """ - - if self.inplace and ext.disable_editable: - print(f"Editable install recognized. Extension '{ext.name}' disabled.") - return - - # Export CMAKE_PREFIX_PATH of all the dependencies - for pkg in ext.cmake_depends_on: - - try: - importlib.import_module(pkg) - except ImportError: - raise ValueError(f"Failed to import '{pkg}'") - - init = importlib.util.find_spec(pkg).origin - BuildExtension.extend_cmake_prefix_path(path=str(Path(init).parent)) - - # The ext_dir directory can be thought as a temporary site-package folder. - # - # Case 1: regular installation. - # ext_dir is the folder that gets compressed to make the wheel archive. When - # installed, the archive is extracted in the active site-package directory. - # Case 2: editable installation. - # ext_dir is the in-source folder containing the Python packages. In this case, - # the CMake project is installed in-source. - ext_dir = Path(self.get_ext_fullpath(ext.name)).parent.absolute() - cmake_install_prefix = ext_dir / ext.install_prefix - - # Initialize the CMake configuration arguments - configure_args = [] - - # Select the appropriate generator and accompanying settings - if ext.cmake_generator is not None: - configure_args += [f"-G \"{ext.cmake_generator}\""] - - if ext.cmake_generator == "Ninja": - # Fix #26: https://github.com/diegoferigo/cmake-build-extension/issues/26 - configure_args += [f"-DCMAKE_MAKE_PROGRAM={shutil.which('ninja')}"] - - # CMake configure arguments - configure_args += [ - f"-DCMAKE_BUILD_TYPE={ext.cmake_build_type}", - f"-DCMAKE_INSTALL_PREFIX:PATH={cmake_install_prefix}", - ] - - # Extend the configure arguments with those passed from the extension - configure_args += ext.cmake_configure_options - - # CMake build arguments - build_args = ["--config", ext.cmake_build_type] - - if platform.system() == "Windows": - - configure_args += [f"-A {self.__get_msvc_arch()}"] - - elif platform.system() in {"Linux", "Darwin"}: - - configure_args += [] - - else: - raise RuntimeError(f"Unsupported '{platform.system()}' platform") - - # Parse the optional CMake options. They can be passed as: - # - # python setup.py build_ext -D"BAR=Foo;VAR=TRUE" - # python setup.py bdist_wheel build_ext -D"BAR=Foo;VAR=TRUE" - # python setup.py install build_ext -D"BAR=Foo;VAR=TRUE" - # python setup.py install -e build_ext -D"BAR=Foo;VAR=TRUE" - # pip install --global-option="build_ext" --global-option="-DBAR=Foo;VAR=TRUE" . - # - configure_args += self.cmake_defines - - # Get the absolute path to the build folder - build_folder = str(Path(".").absolute() / f"{self.build_temp}_{ext.name}") - - # Make sure that the build folder exists - Path(build_folder).mkdir(exist_ok=True, parents=True) - - # 1. Compose CMake configure command - configure_command = [ - "cmake", - "-S", - ext.source_dir, - "-B", - build_folder, - ] + configure_args - - # 2. Compose CMake build command - build_command = ["cmake", "--build", build_folder] + build_args - - # 3. Compose CMake install command - install_command = ["cmake", "--install", build_folder] - - # If the cmake_component option of the CMakeExtension is used, install just - # the specified component. - if self.component is None and ext.cmake_component is not None: - install_command.extend(["--component", ext.cmake_component]) - - # Instead, if the `--component` command line option is used, install just - # the specified component. This has higher priority than what specified in - # the CMakeExtension. - if self.component is not None: - install_command.extend(["--component", self.component]) - - print("") - print("==> Configuring:") - print(f"$ {' '.join(configure_command)}") - subprocess.check_call(configure_command) - print("") - print("==> Building:") - print(f"$ {' '.join(build_command)}") - subprocess.check_call(build_command) - print("") - print("==> Installing:") - print(f"$ {' '.join(install_command)}") - subprocess.check_call(install_command) - print("") - - # Write content to the top-level __init__.py - if ext.write_top_level_init is not None: - with open(file=cmake_install_prefix / "__init__.py", mode="w") as f: - f.write(ext.write_top_level_init) - - # Write content to the bin/__main__.py magic file to expose binaries - if len(ext.expose_binaries) > 0: - bin_dirs = {str(Path(d).parents[0]) for d in ext.expose_binaries} - - import inspect - - main_py = inspect.cleandoc( - f""" - from pathlib import Path - import subprocess - import sys - - def main(): - - binary_name = Path(sys.argv[0]).name - prefix = Path(__file__).parent.parent - bin_dirs = {str(bin_dirs)} - - binary_path = "" - - for dir in bin_dirs: - path = prefix / Path(dir) / binary_name - if path.is_file(): - binary_path = str(path) - break - - path = Path(str(path) + ".exe") - if path.is_file(): - binary_path = str(path) - break - - if not Path(binary_path).is_file(): - name = binary_path if binary_path != "" else binary_name - raise RuntimeError(f"Failed to find binary: {{ name }}") - - sys.argv[0] = binary_path - - result = subprocess.run(args=sys.argv, capture_output=False) - exit(result.returncode) - - if __name__ == "__main__" and len(sys.argv) > 1: - sys.argv = sys.argv[1:] - main()""" - ) - - bin_folder = cmake_install_prefix / "bin" - Path(bin_folder).mkdir(exist_ok=True, parents=True) - with open(file=bin_folder / "__main__.py", mode="w") as f: - f.write(main_py) - - @staticmethod - def extend_cmake_prefix_path(path: str) -> None: - - abs_path = Path(path).absolute() - - if not abs_path.exists(): - raise ValueError(f"Path {abs_path} does not exist") - - if "CMAKE_PREFIX_PATH" in os.environ: - os.environ[ - "CMAKE_PREFIX_PATH" - ] = f"{str(path)}:{os.environ['CMAKE_PREFIX_PATH']}" - else: - os.environ["CMAKE_PREFIX_PATH"] = str(path) diff --git a/third_party/SimpleBLE/simplepyble/cmake_build_extension/cmake_extension.py b/third_party/SimpleBLE/simplepyble/cmake_build_extension/cmake_extension.py deleted file mode 100644 index 248e1a021..000000000 --- a/third_party/SimpleBLE/simplepyble/cmake_build_extension/cmake_extension.py +++ /dev/null @@ -1,58 +0,0 @@ -from pathlib import Path -from typing import List - -from setuptools import Extension - - -class CMakeExtension(Extension): - """ - Custom setuptools extension that configures a CMake project. - - Args: - name: The name of the extension. - install_prefix: The path relative to the site-package directory where the CMake - project is installed (typically the name of the Python package). - disable_editable: Skip this extension in editable mode. - write_top_level_init: Create a new top-level ``__init__.py`` file in the install - prefix and write content. - cmake_configure_options: List of additional CMake configure options (-DBAR=FOO). - source_dir: The location of the main CMakeLists.txt. - cmake_build_type: The default build type of the CMake project. - cmake_component: The name of component to install. Defaults to all components. - cmake_depends_on: List of dependency packages containing required CMake projects. - expose_binaries: List of binary paths to expose, relative to top-level directory. - """ - - def __init__( - self, - name: str, - install_prefix: str = "", - disable_editable: bool = False, - write_top_level_init: str = None, - cmake_configure_options: List[str] = (), - source_dir: str = str(Path(".").absolute()), - cmake_build_type: str = "Release", - cmake_component: str = None, - cmake_depends_on: List[str] = (), - expose_binaries: List[str] = (), - cmake_generator: str = "Ninja", - ): - - super().__init__(name=name, sources=[]) - - if not Path(source_dir).is_absolute(): - source_dir = str(Path(".").absolute() / source_dir) - - if not Path(source_dir).absolute().is_dir(): - raise ValueError(f"Directory '{source_dir}' does not exist") - - self.install_prefix = install_prefix - self.cmake_build_type = cmake_build_type - self.disable_editable = disable_editable - self.write_top_level_init = write_top_level_init - self.cmake_depends_on = cmake_depends_on - self.source_dir = str(Path(source_dir).absolute()) - self.cmake_configure_options = cmake_configure_options - self.cmake_component = cmake_component - self.expose_binaries = expose_binaries - self.cmake_generator = cmake_generator diff --git a/third_party/SimpleBLE/simplepyble/cmake_build_extension/sdist_command.py b/third_party/SimpleBLE/simplepyble/cmake_build_extension/sdist_command.py deleted file mode 100644 index 5f7738d32..000000000 --- a/third_party/SimpleBLE/simplepyble/cmake_build_extension/sdist_command.py +++ /dev/null @@ -1,145 +0,0 @@ -import abc -import os -from pathlib import Path -from shutil import copy2 -from typing import Generator, List - -import setuptools.command.sdist - - -class GitSdistABC(abc.ABC, setuptools.command.sdist.sdist): - """ - This class defines a custom command to build source distribution. It covers the case - where projects store the setup.cfg file in a subfolder and are git repositories. - - The problem in this setup is that only the subfolder by default is packaged - in the sdist, resulting in a archive that only contains part of the whole repo. - """ - - def make_release_tree(self, base_dir, files) -> None: - """ - This method is the responsible of building the list of files that are included - in the source distribution. - - These files (that could be the entire git tree) are copied in the subfolder - before creating the archive, resulting in a source distribution like if the - setup.cfg would be part of the top-level folder. - - This method prepares the egg metadata for the source distribution and allows - specifying a list of files that are copied in the location of the setup.cfg. - """ - - import setuptools_scm.integration - - # Build the setuptools_scm configuration, containing useful info for the sdist - config: setuptools_scm.integration.Configuration = ( - setuptools_scm.integration.Configuration.from_file( - dist_name=self.distribution.metadata.name - ) - ) - - # Get the root of the git repository - repo_root = config.absolute_root - - if not Path(repo_root).exists() or not Path(repo_root).is_dir(): - raise RuntimeError(f"Failed to find a git repo in {repo_root}") - - # Prepare the release tree by calling the original method - super(GitSdistABC, self).make_release_tree(base_dir=base_dir, files=files) - - # Collect all the files and copy them in the subfolder containing setup.cfg - for file in self.get_sdist_files(repo_root=repo_root): - - src = Path(file) - dst = Path(base_dir) / Path(file).relative_to(repo_root) - - # Make sure that the parent directory of the destination exists - dst.absolute().parent.mkdir(parents=True, exist_ok=True) - - print(f"{Path(file).relative_to(repo_root)} -> {dst}") - copy2(src=src, dst=dst) - - # Create the updated list of files included in the sdist - all_files_gen = Path(base_dir).glob(pattern="**/*") - all_files = [str(f.relative_to(base_dir)) for f in all_files_gen] - - # Find the SOURCES.txt file - sources_txt_list = list(Path(base_dir).glob(pattern=f"*.egg-info/SOURCES.txt")) - assert len(sources_txt_list) == 1 - - # Update the SOURCES.txt files with the real content of the sdist - os.unlink(sources_txt_list[0]) - with open(file=sources_txt_list[0], mode="w") as f: - f.write("\n".join([str(f) for f in all_files])) - - @staticmethod - @abc.abstractmethod - def get_sdist_files(repo_root: str) -> List[Path]: - """ - Return all files that will be included in the source distribution. - - Note: the egg metadata are included by default, there's not need to specify them. - - Args: - repo_root: The path to the root of the git repository. - - Returns: - The list of files to include in the source distribution. - """ - - -class GitSdistTree(GitSdistABC): - """ - This class defines a custom command to build source distribution. It covers the case - where projects store the setup.cfg file in a subfolder and are git repositories. - - The problem in this setup is that only the subfolder by default is packaged - in the sdist, resulting in a archive that only contains part of the whole repo. - - In particular, this class copies all the files that are part of the last git commit. - Any uncommitted or staged files are ignored and are not part of the sdist. - """ - - @staticmethod - def get_sdist_files(repo_root: str) -> List[Path]: - - import git - - # Create the git Repo object - git_repo = git.Repo(path=repo_root) - - # Get all the files of the git repo recursively - def list_paths( - tree: git.Tree, path: Path = Path(".") - ) -> Generator[Path, None, None]: - - for blob in tree.blobs: - yield path / blob.name - - for tree in tree.trees: - yield from list_paths(tree=tree, path=path / tree.name) - - # Return the list of absolute paths to all the git repo files - return list(list_paths(tree=git_repo.commit().tree, path=Path(repo_root))) - - -class GitSdistFolder(GitSdistABC): - """ - This class defines a custom command to build source distribution. It covers the case - where projects store the setup.cfg file in a subfolder and are git repositories. - - The problem in this setup is that only the subfolder by default is packaged - in the sdist, resulting in a archive that only contains part of the whole repo. - - In particular, this class copies all the files that are part of the git folder. - It includes also all uncommitted and staged files. - """ - - @staticmethod - def get_sdist_files(repo_root: str) -> List[Path]: - - # Create the list of files of the git folder - all_files_gen = Path(repo_root).glob(pattern="**/*") - - # Return the list of absolute paths to all the git folder files (also uncommited) - return [f for f in all_files_gen if not f.is_dir() and ".git" not in f.parts] \ No newline at end of file diff --git a/third_party/SimpleBLE/simplepyble/requirements.txt b/third_party/SimpleBLE/simplepyble/requirements.txt index e3142d4c2..4efdab7d9 100644 --- a/third_party/SimpleBLE/simplepyble/requirements.txt +++ b/third_party/SimpleBLE/simplepyble/requirements.txt @@ -1,7 +1,10 @@ twine +build +auditwheel wheel -ninja +ninja; platform_system!='Windows' pytest pybind11 -setuptools -cibuildwheel==2.9 +scikit-build +cmake>=3.21 +setuptools>=42 diff --git a/third_party/SimpleBLE/simplepyble/src/main.cpp b/third_party/SimpleBLE/simplepyble/src/main.cpp index f1694ac9a..e2a48ec91 100644 --- a/third_party/SimpleBLE/simplepyble/src/main.cpp +++ b/third_party/SimpleBLE/simplepyble/src/main.cpp @@ -13,13 +13,14 @@ namespace py = pybind11; +void wrap_types(py::module& m); void wrap_descriptor(py::module& m); void wrap_characteristic(py::module& m); void wrap_service(py::module& m); void wrap_peripheral(py::module& m); void wrap_adapter(py::module& m); -PYBIND11_MODULE(simplepyble, m) { +PYBIND11_MODULE(_simplepyble, m) { m.attr("__version__") = SIMPLEPYBLE_VERSION; // m.doc() = R"pbdoc( @@ -39,6 +40,11 @@ PYBIND11_MODULE(simplepyble, m) { Returns the currently-running operating system. )pbdoc"); + m.def("get_simpleble_version", &SimpleBLE::get_simpleble_version, R"pbdoc( + Returns the version of SimpleBLE. + )pbdoc"); + + wrap_types(m); wrap_descriptor(m); wrap_characteristic(m); wrap_service(m); diff --git a/third_party/SimpleBLE/simplepyble/src/simplepyble/__init__.py b/third_party/SimpleBLE/simplepyble/src/simplepyble/__init__.py new file mode 100644 index 000000000..fddd3147d --- /dev/null +++ b/third_party/SimpleBLE/simplepyble/src/simplepyble/__init__.py @@ -0,0 +1 @@ +from ._simplepyble import * diff --git a/third_party/SimpleBLE/simplepyble/src/wrap_characteristic.cpp b/third_party/SimpleBLE/simplepyble/src/wrap_characteristic.cpp index 8c1c932f2..411b45bcb 100644 --- a/third_party/SimpleBLE/simplepyble/src/wrap_characteristic.cpp +++ b/third_party/SimpleBLE/simplepyble/src/wrap_characteristic.cpp @@ -18,9 +18,39 @@ constexpr auto kDocsCharacteristicDescriptors = R"pbdoc( Descriptors of the characteristic )pbdoc"; +constexpr auto kDocsCharacteristicCapabilities = R"pbdoc( + Capabilities of the characteristic +)pbdoc"; + +constexpr auto kDocsCharacteristicCanRead = R"pbdoc( + Whether the characteristic can be read +)pbdoc"; + +constexpr auto kDocsCharacteristicCanWriteRequest = R"pbdoc( + Whether the characteristic can be written with a request +)pbdoc"; + +constexpr auto kDocsCharacteristicCanWriteCommand = R"pbdoc( + Whether the characteristic can be written with a command +)pbdoc"; + +constexpr auto kDocsCharacteristicCanNotify = R"pbdoc( + Whether the characteristic can be notified +)pbdoc"; + +constexpr auto kDocsCharacteristicCanIndicate = R"pbdoc( + Whether the characteristic can be indicated +)pbdoc"; + void wrap_characteristic(py::module& m) { // TODO: Add __str__ and __repr__ methods py::class_(m, "Characteristic", kDocsCharacteristic) .def("uuid", &SimpleBLE::Characteristic::uuid, kDocsCharacteristicUuid) - .def("descriptors", &SimpleBLE::Characteristic::descriptors, kDocsCharacteristicDescriptors); + .def("descriptors", &SimpleBLE::Characteristic::descriptors, kDocsCharacteristicDescriptors) + .def("capabilities", &SimpleBLE::Characteristic::capabilities, kDocsCharacteristicCapabilities) + .def("can_read", &SimpleBLE::Characteristic::can_read, kDocsCharacteristicCanRead) + .def("can_write_request", &SimpleBLE::Characteristic::can_write_request, kDocsCharacteristicCanWriteRequest) + .def("can_write_command", &SimpleBLE::Characteristic::can_write_command, kDocsCharacteristicCanWriteCommand) + .def("can_notify", &SimpleBLE::Characteristic::can_notify, kDocsCharacteristicCanNotify) + .def("can_indicate", &SimpleBLE::Characteristic::can_indicate, kDocsCharacteristicCanIndicate); } diff --git a/third_party/SimpleBLE/simplepyble/src/wrap_peripheral.cpp b/third_party/SimpleBLE/simplepyble/src/wrap_peripheral.cpp index cdbb15f02..af6595398 100644 --- a/third_party/SimpleBLE/simplepyble/src/wrap_peripheral.cpp +++ b/third_party/SimpleBLE/simplepyble/src/wrap_peripheral.cpp @@ -22,10 +22,22 @@ constexpr auto kDocsPeripheralAddress = R"pbdoc( Address of the peripheral )pbdoc"; +constexpr auto kDocsPeripheralAddressType = R"pbdoc( + Address Type of the peripheral +)pbdoc"; + constexpr auto kDocsPeripheralRSSI = R"pbdoc( RSSI of the peripheral )pbdoc"; +constexpr auto kDocsPeripheralTxPower = R"pbdoc( + Transit Power of the peripheral in dBm +)pbdoc"; + +constexpr auto kDocsPeripheralMtu = R"pbdoc( + Get the negotiated MTU value +)pbdoc"; + constexpr auto kDocsPeripheralConnect = R"pbdoc( Connect to the peripheral )pbdoc"; @@ -98,13 +110,18 @@ constexpr auto kDocsPeripheralSetCallbackOnDisconnected = R"pbdoc( Set callback on disconnected )pbdoc"; +// clang-format off + void wrap_peripheral(py::module& m) { // TODO: Add __str__ and __repr__ methods py::class_(m, "Peripheral", kDocsPeripheral) .def("initialized", &SimpleBLE::Peripheral::initialized, kDocsPeripheralInitialized) .def("identifier", &SimpleBLE::Peripheral::identifier, kDocsPeripheralIdentifier) .def("address", &SimpleBLE::Peripheral::address, kDocsPeripheralAddress) + .def("address_type", &SimpleBLE::Peripheral::address_type, kDocsPeripheralAddressType) .def("rssi", &SimpleBLE::Peripheral::rssi, kDocsPeripheralRSSI) + .def("tx_power", &SimpleBLE::Peripheral::tx_power, kDocsPeripheralTxPower) + .def("mtu", &SimpleBLE::Peripheral::mtu, kDocsPeripheralMtu) .def("connect", &SimpleBLE::Peripheral::connect, py::call_guard(), kDocsPeripheralConnect) .def("disconnect", &SimpleBLE::Peripheral::disconnect, kDocsPeripheralDisconnect) @@ -132,40 +149,53 @@ void wrap_peripheral(py::module& m) { .def( "write_request", [](SimpleBLE::Peripheral& p, std::string service, std::string characteristic, py::bytes payload) { - p.write_request(service, characteristic, payload); + // Note py::bytes implicitly converts to std::string + SimpleBLE::ByteArray cpp_payload(payload); + py::gil_scoped_release release; + p.write_request(service, characteristic, cpp_payload); }, kDocsPeripheralWriteRequest) .def( "write_command", [](SimpleBLE::Peripheral& p, std::string service, std::string characteristic, py::bytes payload) { - p.write_command(service, characteristic, payload); + // Note py::bytes implicitly converts to std::string + SimpleBLE::ByteArray cpp_payload(payload); + py::gil_scoped_release release; + p.write_command(service, characteristic, cpp_payload); }, kDocsPeripheralWriteCommand) .def( "notify", - [](SimpleBLE::Peripheral& p, std::string service, std::string characteristic, - std::function cb) { - p.notify(service, characteristic, [cb](SimpleBLE::ByteArray payload) { cb(py::bytes(payload)); }); + [](SimpleBLE::Peripheral& p, std::string service, std::string characteristic, std::function cb) { + p.notify(service, characteristic, [cb](SimpleBLE::ByteArray payload) { + py::gil_scoped_acquire gil; + cb(py::bytes(payload)); + }); }, kDocsPeripheralNotify) .def( "indicate", - [](SimpleBLE::Peripheral& p, std::string service, std::string characteristic, - std::function cb) { - p.indicate(service, characteristic, [cb](SimpleBLE::ByteArray payload) { cb(py::bytes(payload)); }); + [](SimpleBLE::Peripheral& p, std::string service, std::string characteristic, std::function cb) { + p.indicate(service, characteristic, [cb](SimpleBLE::ByteArray payload) { + py::gil_scoped_acquire gil; + cb(py::bytes(payload)); + }); }, kDocsPeripheralIndicate) .def("unsubscribe", &SimpleBLE::Peripheral::unsubscribe, kDocsPeripheralUnsubscribe) .def( "descriptor_read", - [](SimpleBLE::Peripheral& p, std::string const& service, std::string const& characteristic, - std::string const& descriptor) { return py::bytes(p.read(service, characteristic, descriptor)); }, + [](SimpleBLE::Peripheral& p, std::string const& service, std::string const& characteristic, std::string const& descriptor) { + return py::bytes(p.read(service, characteristic, descriptor)); + }, kDocsPeripheralDescriptorRead) .def( "descriptor_write", - [](SimpleBLE::Peripheral& p, std::string service, std::string characteristic, std::string const& descriptor, - py::bytes payload) { p.write(service, characteristic, descriptor, payload); }, + [](SimpleBLE::Peripheral& p, std::string service, std::string characteristic, std::string const& descriptor, py::bytes payload) { + // Note py::bytes implicitly converts to std::string + p.write(service, characteristic, descriptor, SimpleBLE::ByteArray(payload)); + }, kDocsPeripheralDescriptorWrite) .def("set_callback_on_connected", &SimpleBLE::Peripheral::set_callback_on_connected, py::keep_alive<1, 2>(), @@ -173,3 +203,5 @@ void wrap_peripheral(py::module& m) { .def("set_callback_on_disconnected", &SimpleBLE::Peripheral::set_callback_on_disconnected, py::keep_alive<1, 2>(), kDocsPeripheralSetCallbackOnDisconnected); } + +// clang-format on \ No newline at end of file diff --git a/third_party/SimpleBLE/simplepyble/src/wrap_service.cpp b/third_party/SimpleBLE/simplepyble/src/wrap_service.cpp index 81dab82c9..b5d4afe61 100644 --- a/third_party/SimpleBLE/simplepyble/src/wrap_service.cpp +++ b/third_party/SimpleBLE/simplepyble/src/wrap_service.cpp @@ -14,6 +14,10 @@ constexpr auto kDocsServiceUuid = R"pbdoc( UUID of the service )pbdoc"; +constexpr auto kDocsServiceData = R"pbdoc( + Advertised service data +)pbdoc"; + constexpr auto kDocsServiceCharacteristics = R"pbdoc( Characteristics of the service )pbdoc"; @@ -22,5 +26,7 @@ void wrap_service(py::module& m) { // TODO: Add __str__ and __repr__ methods py::class_(m, "Service", kDocsService) .def("uuid", &SimpleBLE::Service::uuid, kDocsServiceUuid) + .def( + "data", [](SimpleBLE::Service& s) { return py::bytes(s.data()); }, kDocsServiceData) .def("characteristics", &SimpleBLE::Service::characteristics, kDocsServiceCharacteristics); } diff --git a/third_party/SimpleBLE/simplepyble/src/wrap_types.cpp b/third_party/SimpleBLE/simplepyble/src/wrap_types.cpp new file mode 100644 index 000000000..93f8b3c23 --- /dev/null +++ b/third_party/SimpleBLE/simplepyble/src/wrap_types.cpp @@ -0,0 +1,21 @@ +#include +#include +#include + +#include "simpleble/Types.h" + +namespace py = pybind11; + +void wrap_types(py::module& m) { + py::enum_(m, "OperatingSystem") + .value("WINDOWS", SimpleBLE::OperatingSystem::WINDOWS) + .value("MACOS", SimpleBLE::OperatingSystem::MACOS) + .value("LINUX", SimpleBLE::OperatingSystem::LINUX) + .export_values(); + + py::enum_(m, "BluetoothAddressType") + .value("PUBLIC", SimpleBLE::BluetoothAddressType::PUBLIC) + .value("RANDOM", SimpleBLE::BluetoothAddressType::RANDOM) + .value("UNSPECIFIED", SimpleBLE::BluetoothAddressType::UNSPECIFIED) + .export_values(); +} diff --git a/third_party/SimpleBLE/simplersble/README.md b/third_party/SimpleBLE/simplersble/README.md new file mode 100644 index 000000000..013873611 --- /dev/null +++ b/third_party/SimpleBLE/simplersble/README.md @@ -0,0 +1,53 @@ +# SimpleRsBLE + +The ultimate fully-fledged cross-platform library and bindings for Bluetooth Low Energy (BLE). + +## Overview + +The [SimpleBLE](https://github.com/OpenBluetoothToolbox/SimpleBLE/) project aims to provide +fully cross-platform BLE libraries and bindings, designed for simplicity and ease of use +with a licencing scheme chosen to be friendly towards commercial use. All specific operating +system quirks are handled internally to provide a consistent behavior across all platforms. +The libraries also provide first-class support for vendorization of all third-party +dependencies, allowing for easy integration into existing projects. + +If you want to use SimpleRsBLE and need help. **Please do not hesitate to reach out!** + +- Join our [Discord](https://discord.gg/N9HqNEcvP3) server. +- Contact me: `kevin at dewald dot me` + +## Supported platforms + +- Windows: Windows 10+ +- Linux: Ubuntu 20.04+ +- MacOS: 10.15+ (except 12.0, 12.1, and 12.2) +- iOS: 15.0+ + +## Usage + +You can add SimpleRsBLE to your project by adding the following lines to your `Cargo.toml`: + +```toml +[dependencies] +simplersble = "0.6.0" +``` + +Please review our [code examples](https://github.com/OpenBluetoothToolbox/SimpleBLE/tree/main/examples/simplersble/src/bin) +on GitHub for more information on how to use SimpleRsBLE. + +## Contributing + +Pull requests are welcome. For major changes, please open an issue first to discuss +what you would like to change. + +# License + +Since February 15th 2024, SimpleBLE is now available under the GNU General Public License +version 3 (GPLv3), with the option for a commercial license without the GPLv3 restrictions +available for a fee. (More information on pricing and commercial terms of service will be +available soon.) + +To enquire about a commercial license, please contact us at `contact at simpleble dot org`. + +Likewise, if you are using SimpleBLE in an open-source project and would like to request +a free commercial license or if you have any other questions, please reach out at `contact at simpleble dot org`. diff --git a/third_party/SimpleBLE/simplersble/build.rs b/third_party/SimpleBLE/simplersble/build.rs new file mode 100644 index 000000000..64c2b3151 --- /dev/null +++ b/third_party/SimpleBLE/simplersble/build.rs @@ -0,0 +1,62 @@ +use cmake; +use cxx_build; +use std::env; +use std::path::Path; + +fn compile_simpleble() { + let build_debug = env::var("DEBUG").unwrap() == "true"; + if build_debug { println!("cargo:warning=Building in DEBUG mode"); } + + let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let simplersble_source_path = Path::new(&cargo_manifest_dir).join("simplersble"); + + println!("cargo:warning=CWD: {}", env::current_dir().unwrap().display()); + println!("cargo:warning=ENV: {} - {}", "OUT_DIR", env::var("OUT_DIR").unwrap()); + println!("cargo:warning=ENV: {} - {}", "CARGO_MANIFEST_DIR", env::var("CARGO_MANIFEST_DIR").unwrap()); + println!("cargo:warning=ENV: {} - {}", "CARGO_PKG_NAME", env::var("CARGO_PKG_NAME").unwrap()); + println!("cargo:warning=ENV: {} - {}", "CARGO_PKG_VERSION", env::var("CARGO_PKG_VERSION").unwrap()); + + // The simpleble library name depends if we're building in debug more or not. + let simpleble_library_name = if build_debug {"simpleble-debug"} else {"simpleble"}; + let simpleble_build_dest = cmake::Config::new("simpleble").build(); + let simpleble_include_path = Path::new(&simpleble_build_dest).join("include"); + + cxx_build::CFG.exported_header_dirs.push(&simpleble_include_path); + cxx_build::CFG.exported_header_dirs.push(&simplersble_source_path); + + println!("cargo:rustc-link-search=native={}/lib", simpleble_build_dest.display()); + println!("cargo:rustc-link-lib=static={}", simpleble_library_name); + + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); + match target_os.as_str() { + "macos" => { + println!("cargo:rustc-link-lib=framework=Foundation"); + println!("cargo:rustc-link-lib=framework=CoreBluetooth"); + }, + "windows" => {}, + "linux" => { + println!("cargo:rustc-link-lib=dbus-1"); + }, + &_ => panic!("Unexpected target OS") + } +} + +fn main() { + // TODO: Add all files that would trigger a rerun + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=src/lib.rs"); + println!("cargo:rerun-if-changed=src/bindings/Bindings.hpp"); + println!("cargo:rerun-if-changed=src/bindings/Bindings.cpp"); + + compile_simpleble(); + + if std::env::var("DOCS_RS").is_ok() { + println!("cargo:warning=Building DOCS"); + } + + // Build the bindings + cxx_build::bridge("simplersble/src/lib.rs") + .file("simplersble/src/bindings/Bindings.cpp") + .flag_if_supported("-std=c++17") + .compile("simpleble_bindings"); +} diff --git a/third_party/SimpleBLE/simplersble/src/bindings/Bindings.cpp b/third_party/SimpleBLE/simplersble/src/bindings/Bindings.cpp new file mode 100644 index 000000000..762260989 --- /dev/null +++ b/third_party/SimpleBLE/simplersble/src/bindings/Bindings.cpp @@ -0,0 +1,328 @@ +#include "Bindings.hpp" +#include + +#include "simplersble/simplersble/src/lib.rs.h" + +rust::Vec Bindings::RustyAdapter_get_adapters() { + rust::Vec result; + + for (auto& adapter : SimpleBLE::Adapter::get_adapters()) { + Bindings::RustyAdapterWrapper wrapper; + wrapper.internal = std::make_unique(adapter); + result.push_back(std::move(wrapper)); + } + + return result; +} + +bool Bindings::RustyAdapter_bluetooth_enabled() { return SimpleBLE::Adapter::bluetooth_enabled(); } + +// Adapter Bindings + +void Bindings::RustyAdapter::link(SimpleRsBLE::Adapter& target) const { + // Time to explain the weird shenanigan we're doing here: + // The TL;DR is that we're making the Adapter(Rust) and the RustyAdapter(C++) + // point to each other in a safe way. + // To achieve this, the Adapter(Rust) owns a RustyAdapter(C++) via a UniquePtr, + // which ensures that calls will always be made to a valid C++ object. + // We now give the RustyAdapter(C++) a pointer back to the Adapter(Rust), + // so that callbacks can be forwarded back to the Rust domain. + // In order to ensure that the Adapter(Rust) is always valid (given + // that Rust is keen on moving stuff around) the object is created as a + // Pin> + + // `_adapter` is a pointer to a pointer, allowing us to manipulate the contents within const functions. + *_adapter = ⌖ // THIS LINE IS SUPER IMPORTANT + + _internal->set_callback_on_scan_start([this]() { + SimpleRsBLE::Adapter* p_adapter = *this->_adapter; + if (p_adapter == nullptr) return; + + p_adapter->on_callback_scan_start(); + }); + + _internal->set_callback_on_scan_stop([this]() { + SimpleRsBLE::Adapter* p_adapter = *this->_adapter; + if (p_adapter == nullptr) return; + + p_adapter->on_callback_scan_stop(); + }); + + _internal->set_callback_on_scan_found([this](SimpleBLE::Peripheral peripheral) { + SimpleRsBLE::Adapter* p_adapter = *this->_adapter; + if (p_adapter == nullptr) return; + + Bindings::RustyPeripheralWrapper wrapper; + wrapper.internal = std::make_unique(peripheral); + p_adapter->on_callback_scan_found(wrapper); + }); + + _internal->set_callback_on_scan_updated([this](SimpleBLE::Peripheral peripheral) { + SimpleRsBLE::Adapter* p_adapter = *this->_adapter; + if (p_adapter == nullptr) return; + + Bindings::RustyPeripheralWrapper wrapper; + wrapper.internal = std::make_unique(peripheral); + p_adapter->on_callback_scan_updated(wrapper); + }); +} + +void Bindings::RustyAdapter::unlink() const { + // `_adapter` is a pointer to a pointer. + *_adapter = nullptr; +} + +rust::String Bindings::RustyAdapter::identifier() const { return rust::String(_internal->identifier()); } + +rust::String Bindings::RustyAdapter::address() const { return rust::String(_internal->address()); } + +void Bindings::RustyAdapter::scan_start() const { _internal->scan_start(); } + +void Bindings::RustyAdapter::scan_stop() const { _internal->scan_stop(); } + +void Bindings::RustyAdapter::scan_for(int32_t timeout_ms) const { _internal->scan_for(timeout_ms); } + +bool Bindings::RustyAdapter::scan_is_active() const { return _internal->scan_is_active(); } + +rust::Vec Bindings::RustyAdapter::scan_get_results() const { + rust::Vec result; + + for (auto& peripheral : _internal->scan_get_results()) { + Bindings::RustyPeripheralWrapper wrapper; + wrapper.internal = std::make_unique(peripheral); + result.push_back(std::move(wrapper)); + } + + return result; +} + +rust::Vec Bindings::RustyAdapter::get_paired_peripherals() const { + rust::Vec result; + + for (auto& peripheral : _internal->get_paired_peripherals()) { + Bindings::RustyPeripheralWrapper wrapper; + wrapper.internal = std::make_unique(peripheral); + result.push_back(std::move(wrapper)); + } + + return result; +} + +// Peripheral Bindings + +void Bindings::RustyPeripheral::link(SimpleRsBLE::Peripheral& target) const { + // Time to explain the weird shenanigan we're doing here: + // The TL;DR is that we're making the Peripheral(Rust) and the RustyPeripheral(C++) + // point to each other in a safe way. + // To achieve this, the Peripheral(Rust) owns a RustyPeripheral(C++) via a UniquePtr, + // which ensures that calls will always be made to a valid C++ object. + // We now give the RustyPeripheral(C++) a pointer back to the Peripheral(Rust), + // so that callbacks can be forwarded back to the Rust domain. + // In order to ensure that the Peripheral(Rust) is always valid (given + // that Rust is keen on moving stuff around) the object is created as a + // Pin> + + // `_peripheral` is a pointer to a pointer, allowing us to manipulate the contents within const functions. + *_peripheral = ⌖ // THIS LINE IS SUPER IMPORTANT + + _internal->set_callback_on_connected([this]() { + SimpleRsBLE::Peripheral* p_peripheral = *this->_peripheral; + if (p_peripheral == nullptr) return; + + p_peripheral->on_callback_connected(); + }); + + _internal->set_callback_on_disconnected([this]() { + SimpleRsBLE::Peripheral* p_peripheral = *this->_peripheral; + if (p_peripheral == nullptr) return; + + p_peripheral->on_callback_disconnected(); + }); +} + +void Bindings::RustyPeripheral::unlink() const { + // `_peripheral` is a pointer to a pointer. + *_peripheral = nullptr; +} + +rust::String Bindings::RustyPeripheral::identifier() const { return rust::String(_internal->identifier()); } + +rust::String Bindings::RustyPeripheral::address() const { return rust::String(_internal->address()); } + +SimpleBLE::BluetoothAddressType Bindings::RustyPeripheral::address_type() const { return _internal->address_type(); } + +int16_t Bindings::RustyPeripheral::rssi() const { return _internal->rssi(); } + +int16_t Bindings::RustyPeripheral::tx_power() const { return _internal->tx_power(); } + +uint16_t Bindings::RustyPeripheral::mtu() const { return _internal->mtu(); } + +void Bindings::RustyPeripheral::connect() const { _internal->connect(); } + +void Bindings::RustyPeripheral::disconnect() const { _internal->disconnect(); } + +bool Bindings::RustyPeripheral::is_connected() const { return _internal->is_connected(); } + +bool Bindings::RustyPeripheral::is_connectable() const { return _internal->is_connectable(); } + +bool Bindings::RustyPeripheral::is_paired() const { return _internal->is_paired(); } + +void Bindings::RustyPeripheral::unpair() const { _internal->unpair(); } + +rust::Vec Bindings::RustyPeripheral::services() const { + rust::Vec result; + + for (auto& service : _internal->services()) { + Bindings::RustyServiceWrapper wrapper; + wrapper.internal = std::make_unique(service); + result.push_back(std::move(wrapper)); + } + + return result; +} + +rust::Vec Bindings::RustyPeripheral::manufacturer_data() const { + rust::Vec result; + + for (auto& manufacturer_data : _internal->manufacturer_data()) { + Bindings::RustyManufacturerDataWrapper wrapper; + wrapper.company_id = manufacturer_data.first; + + for (auto& byte : manufacturer_data.second) { + wrapper.data.push_back(byte); + } + + result.push_back(std::move(wrapper)); + } + + return result; +} + +rust::Vec Bindings::RustyPeripheral::read(rust::String const& service, + rust::String const& characteristic) const { + std::string read_result = _internal->read(std::string(service), std::string(characteristic)); + + rust::Vec result; + for (auto& byte : read_result) { + result.push_back(byte); + } + + return result; +} + +void Bindings::RustyPeripheral::write_request(rust::String const& service_rs, rust::String const& characteristic_rs, + rust::Vec const& data) const { + std::string service(service_rs); + std::string characteristic(characteristic_rs); + std::string data_vec((char*)data.data(), data.size()); + + _internal->write_request(service, characteristic, data_vec); +} + +void Bindings::RustyPeripheral::write_command(rust::String const& service_rs, rust::String const& characteristic_rs, + rust::Vec const& data) const { + std::string service(service_rs); + std::string characteristic(characteristic_rs); + std::string data_vec((char*)data.data(), data.size()); + + _internal->write_command(service, characteristic, data_vec); +} + +void Bindings::RustyPeripheral::notify(rust::String const& service_rs, rust::String const& characteristic_rs) const { + std::string service(service_rs); + std::string characteristic(characteristic_rs); + + _internal->notify(service, characteristic, [this, service_rs, characteristic_rs](std::string data) { + SimpleRsBLE::Peripheral* p_peripheral = *this->_peripheral; + if (p_peripheral == nullptr) return; + + rust::Vec data_vec; + for (auto& byte : data) { + data_vec.push_back(byte); + } + + p_peripheral->on_callback_characteristic_updated(service_rs, characteristic_rs, data_vec); + }); +} + +void Bindings::RustyPeripheral::indicate(rust::String const& service_rs, rust::String const& characteristic_rs) const { + std::string service(service_rs); + std::string characteristic(characteristic_rs); + + _internal->indicate(service, characteristic, [this, service_rs, characteristic_rs](std::string data) { + SimpleRsBLE::Peripheral* p_peripheral = *this->_peripheral; + if (p_peripheral == nullptr) return; + + rust::Vec data_vec; + for (auto& byte : data) { + data_vec.push_back(byte); + } + + p_peripheral->on_callback_characteristic_updated(service_rs, characteristic_rs, data_vec); + }); +} + +void Bindings::RustyPeripheral::unsubscribe(rust::String const& service_rs, + rust::String const& characteristic_rs) const { + std::string service(service_rs); + std::string characteristic(characteristic_rs); + + _internal->unsubscribe(service, characteristic); +} + +rust::Vec Bindings::RustyPeripheral::read_descriptor(rust::String const& service, + rust::String const& characteristic, + rust::String const& descriptor) const { + std::string read_result = _internal->read(std::string(service), std::string(characteristic), + std::string(descriptor)); + + rust::Vec result; + for (auto& byte : read_result) { + result.push_back(byte); + } + + return result; +} + +void Bindings::RustyPeripheral::write_descriptor(rust::String const& service, rust::String const& characteristic, + rust::String const& descriptor, rust::Vec const& data) const { + _internal->write(std::string(service), std::string(characteristic), std::string(descriptor), + std::string((char*)data.data(), data.size())); +} + +// Service Bindings + +rust::Vec Bindings::RustyService::data() const { + rust::Vec result; + for (auto& byte : _internal->data()) { + result.push_back(byte); + } + + return result; +} + +rust::Vec Bindings::RustyService::characteristics() const { + rust::Vec result; + + for (auto& characteristic : _internal->characteristics()) { + Bindings::RustyCharacteristicWrapper wrapper; + wrapper.internal = std::make_unique(characteristic); + result.push_back(std::move(wrapper)); + } + + return result; +} + +// Characteristic Bindings + +rust::Vec Bindings::RustyCharacteristic::descriptors() const { + rust::Vec result; + + for (auto& descriptor : _internal->descriptors()) { + Bindings::RustyDescriptorWrapper wrapper; + wrapper.internal = std::make_unique(descriptor); + result.push_back(std::move(wrapper)); + } + + return result; +} diff --git a/third_party/SimpleBLE/simplersble/src/bindings/Bindings.hpp b/third_party/SimpleBLE/simplersble/src/bindings/Bindings.hpp new file mode 100644 index 000000000..cdc5d7449 --- /dev/null +++ b/third_party/SimpleBLE/simplersble/src/bindings/Bindings.hpp @@ -0,0 +1,177 @@ +#pragma once + +#include "rust/cxx.h" +#include "simpleble/Adapter.h" +#include "simpleble/Peripheral.h" + +#include +#include +#include + +namespace SimpleRsBLE { + +struct Adapter; +struct Peripheral; + +}; // namespace SimpleRsBLE + +namespace Bindings { + +struct RustyAdapterWrapper; +struct RustyPeripheralWrapper; +struct RustyServiceWrapper; +struct RustyCharacteristicWrapper; +struct RustyDescriptorWrapper; +struct RustyManufacturerDataWrapper; + +rust::Vec RustyAdapter_get_adapters(); +bool RustyAdapter_bluetooth_enabled(); + +class RustyAdapter : private SimpleBLE::Adapter { + public: + RustyAdapter() = default; + virtual ~RustyAdapter() { _internal.reset(); } + + RustyAdapter(SimpleBLE::Adapter adapter) + : _internal(new SimpleBLE::Adapter(adapter)), _adapter(std::make_unique()){}; + + void link(SimpleRsBLE::Adapter& target) const; + void unlink() const; + + rust::String identifier() const; + rust::String address() const; + + void scan_start() const; + void scan_stop() const; + void scan_for(int32_t timeout_ms) const; + bool scan_is_active() const; + rust::Vec scan_get_results() const; + + rust::Vec get_paired_peripherals() const; + + private: + // NOTE: All internal properties need to be handled as pointers, + // allowing the calls to RustyAdapter to always be const. + // This might require us to store pointers to pointers, so it's + // important to be careful when handling these. + std::unique_ptr _internal; + std::unique_ptr _adapter; +}; + +class RustyPeripheral : private SimpleBLE::Peripheral { + public: + RustyPeripheral() = default; + virtual ~RustyPeripheral() { _internal.reset(); } + + RustyPeripheral(SimpleBLE::Peripheral peripheral) + : _internal(new SimpleBLE::Peripheral(peripheral)), _peripheral(std::make_unique()) {} + + void link(SimpleRsBLE::Peripheral& target) const; + void unlink() const; + + rust::String identifier() const; + rust::String address() const; + SimpleBLE::BluetoothAddressType address_type() const; + int16_t rssi() const; + + int16_t tx_power() const; + uint16_t mtu() const; + + void connect() const; + void disconnect() const; + bool is_connected() const; + bool is_connectable() const; + bool is_paired() const; + void unpair() const; + + rust::Vec services() const; + rust::Vec manufacturer_data() const; + + rust::Vec read(rust::String const& service, rust::String const& characteristic) const; + void write_request(rust::String const& service, rust::String const& characteristic, + rust::Vec const& data) const; + void write_command(rust::String const& service, rust::String const& characteristic, + rust::Vec const& data) const; + void notify(rust::String const& service, rust::String const& characteristic) const; + void indicate(rust::String const& service, rust::String const& characteristic) const; + void unsubscribe(rust::String const& service, rust::String const& characteristic) const; + + rust::Vec read_descriptor(rust::String const& service, rust::String const& characteristic, + rust::String const& descriptor) const; + void write_descriptor(rust::String const& service, rust::String const& characteristic, + rust::String const& descriptor, rust::Vec const& data) const; + + private: + // NOTE: All internal properties need to be handled as pointers, + // allowing the calls to RustyPeripheral to always be const. + // This might require us to store pointers to pointers, so it's + // important to be careful when handling these. + std::unique_ptr _internal; + std::unique_ptr _peripheral; +}; + +class RustyService : private SimpleBLE::Service { + public: + RustyService() = default; + virtual ~RustyService() = default; + + RustyService(SimpleBLE::Service service) : _internal(new SimpleBLE::Service(service)) {} + + rust::String uuid() const { return rust::String(_internal->uuid()); } + + rust::Vec data() const; + + rust::Vec characteristics() const; + + private: + // NOTE: All internal properties need to be handled as pointers, + // allowing the calls to RustyService to always be const. + // This might require us to store pointers to pointers, so it's + // important to be careful when handling these. + std::shared_ptr _internal; +}; + +class RustyCharacteristic : private SimpleBLE::Characteristic { + public: + RustyCharacteristic() = default; + virtual ~RustyCharacteristic() = default; + + RustyCharacteristic(SimpleBLE::Characteristic characteristic) + : _internal(new SimpleBLE::Characteristic(characteristic)) {} + + rust::String uuid() const { return rust::String(_internal->uuid()); } + + rust::Vec descriptors() const; + + bool can_read() const { return _internal->can_read(); } + bool can_write_request() const { return _internal->can_write_request(); } + bool can_write_command() const { return _internal->can_write_command(); } + bool can_notify() const { return _internal->can_notify(); } + bool can_indicate() const { return _internal->can_indicate(); } + + private: + // NOTE: All internal properties need to be handled as pointers, + // allowing the calls to RustyCharacteristic to always be const. + // This might require us to store pointers to pointers, so it's + // important to be careful when handling these. + std::shared_ptr _internal; +}; + +class RustyDescriptor : private SimpleBLE::Descriptor { + public: + RustyDescriptor() = default; + virtual ~RustyDescriptor() = default; + + RustyDescriptor(SimpleBLE::Descriptor descriptor) : _internal(new SimpleBLE::Descriptor(descriptor)) {} + + rust::String uuid() const { return rust::String(_internal->uuid()); } + + private: + // NOTE: All internal properties need to be handled as pointers, + // allowing the calls to RustyDescriptor to always be const. + // This might require us to store pointers to pointers, so it's + // important to be careful when handling these. + std::shared_ptr _internal; +}; + +}; // namespace Bindings diff --git a/third_party/SimpleBLE/simplersble/src/lib.rs b/third_party/SimpleBLE/simplersble/src/lib.rs new file mode 100644 index 000000000..87e48a805 --- /dev/null +++ b/third_party/SimpleBLE/simplersble/src/lib.rs @@ -0,0 +1,854 @@ +use std::collections::HashMap; +use std::fmt; +use std::mem; +use std::pin::Pin; + +#[cxx::bridge] +mod ffi { + + #[namespace = "Bindings"] + struct RustyAdapterWrapper { + internal: UniquePtr, + } + + #[namespace = "Bindings"] + struct RustyPeripheralWrapper { + internal: UniquePtr, + } + + #[namespace = "Bindings"] + struct RustyServiceWrapper { + internal: UniquePtr, + } + + #[namespace = "Bindings"] + struct RustyCharacteristicWrapper { + internal: UniquePtr, + } + + #[namespace = "Bindings"] + struct RustyDescriptorWrapper { + internal: UniquePtr, + } + + #[namespace = "Bindings"] + struct RustyManufacturerDataWrapper { + company_id: u16, + data: Vec, + } + + #[namespace = "SimpleBLE"] + #[repr(i32)] + enum BluetoothAddressType { + PUBLIC, + RANDOM, + UNSPECIFIED, + } + + #[namespace = "SimpleRsBLE"] + extern "Rust" { + type Adapter; + + fn on_callback_scan_start(self: &mut Adapter); + fn on_callback_scan_stop(self: &mut Adapter); + fn on_callback_scan_updated(self: &mut Adapter, peripheral: &mut RustyPeripheralWrapper); + fn on_callback_scan_found(self: &mut Adapter, peripheral: &mut RustyPeripheralWrapper); + + type Peripheral; + + fn on_callback_connected(self: &mut Peripheral); + fn on_callback_disconnected(self: &mut Peripheral); + fn on_callback_characteristic_updated( + self: &mut Peripheral, + service: &String, + Characteristic: &String, + data: &Vec, + ); + } + + unsafe extern "C++" { + include!("src/bindings/Bindings.hpp"); + + #[namespace = "SimpleBLE"] + type BluetoothAddressType; + + #[namespace = "Bindings"] + type RustyAdapter; + + #[namespace = "Bindings"] + type RustyPeripheral; + + #[namespace = "Bindings"] + type RustyService; + + #[namespace = "Bindings"] + type RustyCharacteristic; + + #[namespace = "Bindings"] + type RustyDescriptor; + + // Common functions + + #[namespace = "Bindings"] + fn RustyAdapter_bluetooth_enabled() -> Result; + + #[namespace = "Bindings"] + fn RustyAdapter_get_adapters() -> Result>; + + // RustyAdapter functions + + fn link(self: &RustyAdapter, target: Pin<&mut Adapter>) -> Result<()>; + fn unlink(self: &RustyAdapter) -> Result<()>; + + fn identifier(self: &RustyAdapter) -> Result; + fn address(self: &RustyAdapter) -> Result; + + fn scan_start(self: &RustyAdapter) -> Result<()>; + fn scan_stop(self: &RustyAdapter) -> Result<()>; + fn scan_for(self: &RustyAdapter, timeout_ms: i32) -> Result<()>; + fn scan_is_active(self: &RustyAdapter) -> Result; + fn scan_get_results(self: &RustyAdapter) -> Result>; + + fn get_paired_peripherals(self: &RustyAdapter) -> Result>; + + // RustyPeripheral functions + + fn link(self: &RustyPeripheral, target: Pin<&mut Peripheral>) -> Result<()>; + fn unlink(self: &RustyPeripheral) -> Result<()>; + + fn identifier(self: &RustyPeripheral) -> Result; + fn address(self: &RustyPeripheral) -> Result; + fn address_type(self: &RustyPeripheral) -> Result; + fn rssi(self: &RustyPeripheral) -> Result; + + fn tx_power(self: &RustyPeripheral) -> Result; + fn mtu(self: &RustyPeripheral) -> Result; + + fn connect(self: &RustyPeripheral) -> Result<()>; + fn disconnect(self: &RustyPeripheral) -> Result<()>; + fn is_connected(self: &RustyPeripheral) -> Result; + fn is_connectable(self: &RustyPeripheral) -> Result; + fn is_paired(self: &RustyPeripheral) -> Result; + fn unpair(self: &RustyPeripheral) -> Result<()>; + + fn services(self: &RustyPeripheral) -> Result>; + fn manufacturer_data(self: &RustyPeripheral) -> Result>; + + fn read( + self: &RustyPeripheral, + service: &String, + characteristic: &String, + ) -> Result>; + fn write_request( + self: &RustyPeripheral, + service: &String, + characteristic: &String, + data: &Vec, + ) -> Result<()>; + fn write_command( + self: &RustyPeripheral, + service: &String, + characteristic: &String, + data: &Vec, + ) -> Result<()>; + fn notify(self: &RustyPeripheral, service: &String, characteristic: &String) -> Result<()>; + fn indicate( + self: &RustyPeripheral, + service: &String, + characteristic: &String, + ) -> Result<()>; + fn unsubscribe( + self: &RustyPeripheral, + service: &String, + characteristic: &String, + ) -> Result<()>; + + fn read_descriptor( + self: &RustyPeripheral, + service: &String, + characteristic: &String, + descriptor: &String, + ) -> Result>; + fn write_descriptor( + self: &RustyPeripheral, + service: &String, + characteristic: &String, + descriptor: &String, + data: &Vec, + ) -> Result<()>; + + // RustyService functions + + fn uuid(self: &RustyService) -> String; + fn data(self: &RustyService) -> Vec; + fn characteristics(self: &RustyService) -> Vec; + + // RustyCharacteristic functions + + fn uuid(self: &RustyCharacteristic) -> String; + fn descriptors(self: &RustyCharacteristic) -> Vec; + + fn can_read(self: &RustyCharacteristic) -> bool; + fn can_write_request(self: &RustyCharacteristic) -> bool; + fn can_write_command(self: &RustyCharacteristic) -> bool; + fn can_notify(self: &RustyCharacteristic) -> bool; + fn can_indicate(self: &RustyCharacteristic) -> bool; + + // RustyDescriptor functions + + fn uuid(self: &RustyDescriptor) -> String; + } +} + +#[derive(Debug, Clone)] +pub struct Error { + msg: String, +} + +#[derive(Debug)] +pub enum BluetoothAddressType { + Public, + Random, + Unspecified, +} + +#[derive(Debug)] +pub enum CharacteristicCapability { + Read, + WriteRequest, + WriteCommand, + Notify, + Indicate, +} + +pub struct Adapter { + internal: cxx::UniquePtr, + on_scan_start: Box, + on_scan_stop: Box, + on_scan_found: Box>) + Send + Sync + 'static>, + on_scan_updated: Box>) + Send + Sync + 'static>, +} + +pub struct Peripheral { + internal: cxx::UniquePtr, + + on_connected: Box, + on_disconnected: Box, + + on_characteristic_update_map: HashMap) + Send + Sync + 'static>>, +} + +pub struct Service { + internal: cxx::UniquePtr, +} + +pub struct Characteristic { + internal: cxx::UniquePtr, +} + +pub struct Descriptor { + internal: cxx::UniquePtr, +} + +impl Adapter { + pub fn bluetooth_enabled() -> Result { + ffi::RustyAdapter_bluetooth_enabled().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn get_adapters() -> Result>>, Error> { + let mut raw_adapter_list = ffi::RustyAdapter_get_adapters().map_err(|e| Error { + msg: e.what().to_string(), + })?; + + let mut adapters = Vec::>>::new(); + for adapter_wrapper in raw_adapter_list.iter_mut() { + adapters.push(Adapter::new(adapter_wrapper)); + } + Ok(adapters) + } + + fn new(wrapper: &mut ffi::RustyAdapterWrapper) -> Pin> { + let this = Self { + internal: cxx::UniquePtr::::null(), + on_scan_start: Box::new(|| {}), + on_scan_stop: Box::new(|| {}), + on_scan_found: Box::new(|_| {}), + on_scan_updated: Box::new(|_| {}), + }; + + // Pin the object to guarantee that its location in memory is + // fixed throughout the lifetime of the application + let mut this_boxed = Box::pin(this); + + // Link `this` to the RustyAdapter + wrapper.internal.link(this_boxed.as_mut()).unwrap(); + + // Copy the RustyAdapter pointer into `this` + mem::swap(&mut this_boxed.internal, &mut wrapper.internal); + + return this_boxed; + } + + pub fn identifier(&self) -> Result { + self.internal.identifier().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn address(&self) -> Result { + self.internal.address().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn scan_start(&self) -> Result<(), Error> { + self.internal.scan_start().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn scan_stop(&self) -> Result<(), Error> { + self.internal.scan_stop().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn scan_for(&self, timeout_ms: i32) -> Result<(), Error> { + self.internal.scan_for(timeout_ms).map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn scan_is_active(&self) -> Result { + self.internal.scan_is_active().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn scan_get_results(&self) -> Result>>, Error> { + let mut raw_peripheral_list = self.internal.scan_get_results().map_err(|e| Error { + msg: e.what().to_string(), + })?; + + let mut peripherals = Vec::>>::new(); + for peripheral_wrapper in raw_peripheral_list.iter_mut() { + peripherals.push(Peripheral::new(peripheral_wrapper)); + } + + return Ok(peripherals); + } + + pub fn get_paired_peripherals(&self) -> Result>>, Error> { + let mut raw_peripheral_list = + self.internal.get_paired_peripherals().map_err(|e| Error { + msg: e.what().to_string(), + })?; + + let mut peripherals = Vec::>>::new(); + for peripheral_wrapper in raw_peripheral_list.iter_mut() { + peripherals.push(Peripheral::new(peripheral_wrapper)); + } + + return Ok(peripherals); + } + + pub fn set_callback_on_scan_start(&mut self, cb: Box) { + self.on_scan_start = cb; + } + + pub fn set_callback_on_scan_stop(&mut self, cb: Box) { + self.on_scan_stop = cb; + } + + pub fn set_callback_on_scan_updated( + &mut self, + cb: Box>) + Send + Sync + 'static>, + ) { + self.on_scan_updated = cb; + } + + pub fn set_callback_on_scan_found( + &mut self, + cb: Box>) + Send + Sync + 'static>, + ) { + self.on_scan_found = cb; + } + + fn on_callback_scan_start(&self) { + (self.on_scan_start)(); + } + + fn on_callback_scan_stop(&self) { + (self.on_scan_stop)(); + } + + fn on_callback_scan_updated(&self, peripheral: &mut ffi::RustyPeripheralWrapper) { + (self.on_scan_updated)(Peripheral::new(peripheral)); + } + + fn on_callback_scan_found(&self, peripheral: &mut ffi::RustyPeripheralWrapper) { + (self.on_scan_found)(Peripheral::new(peripheral)); + } +} + +impl Peripheral { + fn new(wrapper: &mut ffi::RustyPeripheralWrapper) -> Pin> { + let this = Self { + internal: cxx::UniquePtr::::null(), + on_connected: Box::new(|| {}), + on_disconnected: Box::new(|| {}), + on_characteristic_update_map: HashMap::new(), + }; + + // Pin the object to guarantee that its location in memory is + // fixed throughout the lifetime of the application + let mut this_boxed = Box::pin(this); + + // Link `this` to the RustyPeripheral + wrapper.internal.link(this_boxed.as_mut()).unwrap(); + + // Copy the RustyPeripheral pointer into `this` + mem::swap(&mut this_boxed.internal, &mut wrapper.internal); + + return this_boxed; + } + + pub fn identifier(&self) -> Result { + self.internal.identifier().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn address(&self) -> Result { + self.internal.address().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn address_type(&self) -> Result { + let address_type = self.internal.address_type().map_err(|e| Error { + msg: e.what().to_string(), + })?; + + return match address_type { + ffi::BluetoothAddressType::PUBLIC => Ok(BluetoothAddressType::Public), + ffi::BluetoothAddressType::RANDOM => Ok(BluetoothAddressType::Random), + ffi::BluetoothAddressType::UNSPECIFIED => Ok(BluetoothAddressType::Unspecified), + _ => Ok(BluetoothAddressType::Unspecified), + }; + } + + pub fn rssi(&self) -> Result { + self.internal.rssi().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn tx_power(&self) -> Result { + self.internal.tx_power().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn mtu(&self) -> Result { + self.internal.mtu().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn connect(&self) -> Result<(), Error> { + self.internal.connect().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn disconnect(&self) -> Result<(), Error> { + self.internal.disconnect().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn is_connected(&self) -> Result { + self.internal.is_connected().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn is_connectable(&self) -> Result { + self.internal.is_connectable().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn is_paired(&self) -> Result { + self.internal.is_paired().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn unpair(&self) -> Result<(), Error> { + self.internal.unpair().map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn services(&self) -> Result>>, Error> { + let mut raw_services = self.internal.services().map_err(|e| Error { + msg: e.what().to_string(), + })?; + + let mut services = Vec::>>::new(); + for service_wrapper in raw_services.iter_mut() { + services.push(Service::new(service_wrapper)); + } + + Ok(services) + } + + pub fn manufacturer_data(&self) -> Result>, Error> { + let raw_manufacturer_data = self.internal.manufacturer_data().map_err(|e| Error { + msg: e.what().to_string(), + })?; + + let mut manufacturer_data = HashMap::>::new(); + for raw_manuf_data in raw_manufacturer_data.iter() { + manufacturer_data.insert(raw_manuf_data.company_id, raw_manuf_data.data.clone()); + } + + Ok(manufacturer_data) + } + + pub fn read(&self, service: &String, characteristic: &String) -> Result, Error> { + self.internal + .read(service, characteristic) + .map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn write_request( + &self, + service: &String, + characteristic: &String, + data: &Vec, + ) -> Result<(), Error> { + self.internal + .write_request(service, characteristic, data) + .map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn write_command( + &self, + service: &String, + characteristic: &String, + data: &Vec, + ) -> Result<(), Error> { + self.internal + .write_command(service, characteristic, data) + .map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn notify( + &mut self, + service: &String, + characteristic: &String, + cb: Box) + Send + Sync + 'static>, + ) -> Result<(), Error> { + // Make a string joining the service and characteristic, then save it in the map + let key = format!("{}{}", service, characteristic); + self.on_characteristic_update_map.insert(key, cb); + + self.internal + .notify(service, characteristic) + .map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn indicate( + &mut self, + service: &String, + characteristic: &String, + cb: Box) + Send + Sync + 'static>, + ) -> Result<(), Error> { + // Make a string joining the service and characteristic, then save it in the map + let key = format!("{}{}", service, characteristic); + self.on_characteristic_update_map.insert(key, cb); + + self.internal + .indicate(service, characteristic) + .map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn unsubscribe(&mut self, service: &String, characteristic: &String) -> Result<(), Error> { + // Make a string joining the service and characteristic, then remove it from the map + let key = format!("{}{}", service, characteristic); + self.on_characteristic_update_map.remove(&key); + + self.internal + .unsubscribe(service, characteristic) + .map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn descriptor_read( + &self, + service: &String, + characteristic: &String, + descriptor: &String, + ) -> Result, Error> { + self.internal + .read_descriptor(service, characteristic, descriptor) + .map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn descriptor_write( + &self, + service: &String, + characteristic: &String, + descriptor: &String, + data: &Vec, + ) -> Result<(), Error> { + self.internal + .write_descriptor(service, characteristic, descriptor, data) + .map_err(|e| Error { + msg: e.what().to_string(), + }) + } + + pub fn set_callback_on_connected(&mut self, cb: Box) { + self.on_connected = cb; + } + + pub fn set_callback_on_disconnected(&mut self, cb: Box) { + self.on_disconnected = cb; + } + + fn on_callback_connected(&self) { + (self.on_connected)(); + } + + fn on_callback_disconnected(&self) { + (self.on_disconnected)(); + } + + fn on_callback_characteristic_updated( + &self, + service: &String, + characteristic: &String, + data: &Vec, + ) { + // Make a string joining the service and characteristic, then look up the callback and call it. + let key = format!("{}{}", service, characteristic); + + if let Some(cb) = self.on_characteristic_update_map.get(&key) { + (cb)(data.clone()); + } + } +} + +impl Service { + fn new(wrapper: &mut ffi::RustyServiceWrapper) -> Pin> { + let this = Self { + internal: cxx::UniquePtr::::null(), + }; + + // Pin the object to guarantee that its location in memory is + // fixed throughout the lifetime of the application + let mut this_boxed = Box::pin(this); + + // Copy the RustyService pointer into `this` + mem::swap(&mut this_boxed.internal, &mut wrapper.internal); + + return this_boxed; + } + + pub fn uuid(&self) -> String { + return self.internal.uuid(); + } + + pub fn data(&self) -> Vec { + return self.internal.data(); + } + + pub fn characteristics(&self) -> Vec>> { + let mut characteristics = Vec::>>::new(); + + for characteristic_wrapper in self.internal.characteristics().iter_mut() { + characteristics.push(Characteristic::new(characteristic_wrapper)); + } + + return characteristics; + } +} + +impl Characteristic { + fn new(wrapper: &mut ffi::RustyCharacteristicWrapper) -> Pin> { + let this = Self { + internal: cxx::UniquePtr::::null(), + }; + + // Pin the object to guarantee that its location in memory is + // fixed throughout the lifetime of the application + let mut this_boxed = Box::pin(this); + + // Copy the RustyCharacteristic pointer into `this` + mem::swap(&mut this_boxed.internal, &mut wrapper.internal); + + return this_boxed; + } + + pub fn uuid(&self) -> String { + return self.internal.uuid(); + } + + pub fn descriptors(&self) -> Vec>> { + let mut descriptors = Vec::>>::new(); + + for descriptor_wrapper in self.internal.descriptors().iter_mut() { + descriptors.push(Descriptor::new(descriptor_wrapper)); + } + + return descriptors; + } + + pub fn capabilities(&self) -> Vec { + let mut capabilities = Vec::::new(); + + if self.internal.can_read() { + capabilities.push(CharacteristicCapability::Read); + } + + if self.internal.can_write_request() { + capabilities.push(CharacteristicCapability::WriteRequest); + } + + if self.internal.can_write_command() { + capabilities.push(CharacteristicCapability::WriteCommand); + } + + if self.internal.can_notify() { + capabilities.push(CharacteristicCapability::Notify); + } + + if self.internal.can_indicate() { + capabilities.push(CharacteristicCapability::Indicate); + } + + return capabilities; + } + + pub fn can_read(&self) -> bool { + return self.internal.can_read(); + } + + pub fn can_write_request(&self) -> bool { + return self.internal.can_write_request(); + } + + pub fn can_write_command(&self) -> bool { + return self.internal.can_write_command(); + } + + pub fn can_notify(&self) -> bool { + return self.internal.can_notify(); + } + + pub fn can_indicate(&self) -> bool { + return self.internal.can_indicate(); + } +} + +impl Descriptor { + fn new(wrapper: &mut ffi::RustyDescriptorWrapper) -> Pin> { + let this = Self { + internal: cxx::UniquePtr::::null(), + }; + + // Pin the object to guarantee that its location in memory is + // fixed throughout the lifetime of the application + let mut this_boxed = Box::pin(this); + + // Copy the RustyDescriptor pointer into `this` + mem::swap(&mut this_boxed.internal, &mut wrapper.internal); + + return this_boxed; + } + + pub fn uuid(&self) -> String { + return self.internal.uuid(); + } +} + +unsafe impl Sync for Adapter {} + +unsafe impl Sync for Peripheral {} + +unsafe impl Sync for Service {} + +unsafe impl Sync for Characteristic {} + +unsafe impl Sync for Descriptor {} + +unsafe impl Send for Adapter {} + +unsafe impl Send for Peripheral {} + +unsafe impl Send for Service {} + +unsafe impl Send for Characteristic {} + +unsafe impl Send for Descriptor {} + +impl std::error::Error for Error {} + +impl fmt::Display for BluetoothAddressType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BluetoothAddressType::Public => write!(f, "Public"), + BluetoothAddressType::Random => write!(f, "Random"), + BluetoothAddressType::Unspecified => write!(f, "Unspecified"), + } + } +} + +impl fmt::Display for CharacteristicCapability { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CharacteristicCapability::Read => write!(f, "Read"), + CharacteristicCapability::WriteRequest => write!(f, "WriteRequest"), + CharacteristicCapability::WriteCommand => write!(f, "WriteCommand"), + CharacteristicCapability::Notify => write!(f, "Notify"), + CharacteristicCapability::Indicate => write!(f, "Indicate"), + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "SimpleBLE error: {}", self.msg) + } +} + +impl Drop for Adapter { + fn drop(&mut self) { + self.internal.unlink().unwrap(); + } +} + +impl Drop for Peripheral { + fn drop(&mut self) { + self.internal.unlink().unwrap(); + } +} diff --git a/third_party/SimpleBLE/utils/build_android.sh b/third_party/SimpleBLE/utils/build_android.sh new file mode 100644 index 000000000..f2f07c6c3 --- /dev/null +++ b/third_party/SimpleBLE/utils/build_android.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash + +# Some portions of this file where inspired from: +# https://medium.com/@Drew_Stokes/bash-argument-parsing-54f3b81a6a8f + +# If the current script is running in MacOS, print a warning +if [[ "$OSTYPE" == "darwin"* ]]; then + + # Define the realpath function, as MacOs doesn't have it + realpath() { + OURPWD=$PWD + cd "$(dirname "$1")" + LINK=$(readlink "$(basename "$1")") + while [ "$LINK" ]; do + cd "$(dirname "$LINK")" + LINK=$(readlink "$(basename "$1")") + done + REALPATH="$PWD/$(basename "$1")" + cd "$OURPWD" + echo "$REALPATH" + } +fi + +# Parse incoming arguments +PARAMS="" +while (( "$#" )); do + case "$1" in + -c|--clean) + FLAG_CLEAN=0 + shift + ;; + -e|--examples) + FLAG_EXAMPLE=0 + shift + ;; + -d|--deploy) + FLAG_DEPLOY=0 + shift + ;; + -r|--run) + FLAG_RUN=$2 + shift + shift + ;; + -*|--*=) # unsupported flags + echo "Error: Unsupported flag $1" >&2 + exit 1 + ;; + *) # preserve positional arguments + PARAMS="$PARAMS $1" + shift + ;; + esac +done + +# Set positional arguments in their proper place +eval set -- "$PARAMS" + +# Base path definitions +PROJECT_ROOT=$(realpath $(dirname `realpath $0`)/..) +SOURCE_PATH=$PROJECT_ROOT/simpleble +BUILD_PATH=$PROJECT_ROOT/build_simpleble_android +INSTALL_PATH=$BUILD_PATH/install + +EXAMPLE_BUILD_PATH=$PROJECT_ROOT/build_simpleble_android_examples +EXAMPLE_SOURCE_PATH=$PROJECT_ROOT/examples/simpleble + +# If FLAG_CLEAN is set, clean the build directory +if [[ ! -z "$FLAG_CLEAN" ]]; then + rm -rf $BUILD_PATH + rm -rf $EXAMPLE_BUILD_PATH +fi + +# Check if the ANDROID_NDK_HOME environment variable is set +if [ -z "$ANDROID_NDK_HOME" ]; then + echo "The ANDROID_NDK_HOME environment variable is not set. Please set it to the path of your Android NDK installation." + exit 1 +fi + +# These are some hardcoded variables used for my test process. You can change them to fit your needs. +ANDROID_ARCH_ABI="armeabi-v7a" +ANDROID_API=21 +# NOTE: Also look at ANDROID_STL_TYPE +ANDROID_ARGS="-DCMAKE_SYSTEM_NAME=Android -DCMAKE_ANDROID_NDK=$ANDROID_NDK_HOME -DCMAKE_ANDROID_ARCH_ABI=$ANDROID_ARCH_ABI -DCMAKE_ANDROID_API=$ANDROID_API" + +cmake -H$SOURCE_PATH -B $BUILD_PATH $ANDROID_ARGS +cmake --build $BUILD_PATH -j7 +cmake --install $BUILD_PATH --prefix "${INSTALL_PATH}" + +if [[ ! -z "$FLAG_EXAMPLE" ]]; then + cmake -H$EXAMPLE_SOURCE_PATH -B $EXAMPLE_BUILD_PATH $ANDROID_ARGS + cmake --build $EXAMPLE_BUILD_PATH -j7 + + if [[ ! -z "$FLAG_DEPLOY" ]]; then + adb shell rm -rf /data/local/tmp/simpleble + adb shell mkdir /data/local/tmp/simpleble + adb push $EXAMPLE_BUILD_PATH/bin/* /data/local/tmp/simpleble + adb shell chmod +x /data/local/tmp/simpleble/* + fi + + if [[ ! -z "$FLAG_RUN" ]]; then + adb shell /data/local/tmp/simpleble/$FLAG_RUN + fi +fi \ No newline at end of file diff --git a/third_party/SimpleBLE/utils/build_docs.sh b/third_party/SimpleBLE/utils/build_docs.sh new file mode 100644 index 000000000..bb2cdc823 --- /dev/null +++ b/third_party/SimpleBLE/utils/build_docs.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +# pip3 install sphinx-autobuild +# sphinx-autobuild docs build_docs/html \ No newline at end of file diff --git a/third_party/SimpleBLE/utils/build_lib.sh b/third_party/SimpleBLE/utils/build_lib.sh index ade1c1c3a..b46450edb 100644 --- a/third_party/SimpleBLE/utils/build_lib.sh +++ b/third_party/SimpleBLE/utils/build_lib.sh @@ -30,6 +30,10 @@ while (( "$#" )); do FLAG_CLEAN=0 shift ;; + -d|--debug) + FLAG_DEBUG=0 + shift + ;; -s|--shared) FLAG_SHARED=0 shift @@ -126,7 +130,11 @@ if [[ ! -z "$FLAG_CLEAN" ]]; then rm -rf $EXAMPLE_BUILD_PATH fi -cmake -H$SOURCE_PATH -B $BUILD_PATH $BUILD_TEST_ARG $BUILD_SANITIZE_ADDRESS_ARG $BUILD_SANITIZE_THREAD_ARG $BUILD_SHARED_ARG $BUILD_PLAIN $EXTRA_BUILD_ARGS +if [[ ! -z "$FLAG_DEBUG" ]]; then + DEBUG_ARG="-DCMAKE_BUILD_TYPE=Debug" +fi + +cmake $DEBUG_ARG -H$SOURCE_PATH -B $BUILD_PATH $BUILD_TEST_ARG $BUILD_SANITIZE_ADDRESS_ARG $BUILD_SANITIZE_THREAD_ARG $BUILD_SHARED_ARG $BUILD_PLAIN $EXTRA_BUILD_ARGS cmake --build $BUILD_PATH -j7 cmake --install $BUILD_PATH --prefix "${INSTALL_PATH}" @@ -138,6 +146,6 @@ else fi if [[ ! -z "$FLAG_EXAMPLE" ]]; then - cmake -H$EXAMPLE_SOURCE_PATH -B $EXAMPLE_BUILD_PATH $BUILD_EXAMPLE_ARGS $BUILD_SHARED_ARG + cmake $DEBUG_ARG -H$EXAMPLE_SOURCE_PATH -B $EXAMPLE_BUILD_PATH $BUILD_EXAMPLE_ARGS $BUILD_SHARED_ARG cmake --build $EXAMPLE_BUILD_PATH -j7 fi diff --git a/third_party/SimpleBLE/utils/clean_workflows.py b/third_party/SimpleBLE/utils/clean_workflows.py new file mode 100644 index 000000000..93789ca28 --- /dev/null +++ b/third_party/SimpleBLE/utils/clean_workflows.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import urllib.parse +import urllib.request + +API_BASE_URL = "https://api.github.com" +REQUEST_ACCEPT_VERSION = "application/vnd.github.v3+json" +REQUEST_USER_AGENT = "magnetikonline/remove-workflow-run" + + +def github_request( + auth_token, path, method=None, parameter_collection=None, parse_response=True +): + # build base request URL/headers + request_url = f"{API_BASE_URL}/{path}" + header_collection = { + "Accept": REQUEST_ACCEPT_VERSION, + "Authorization": f"token {auth_token}", + "User-Agent": REQUEST_USER_AGENT, + } + + if method is None: + # GET method + if parameter_collection is not None: + request_url = ( + f"{request_url}?{urllib.parse.urlencode(parameter_collection)}" + ) + + request = urllib.request.Request(headers=header_collection, url=request_url) + else: + # POST/PATCH/PUT/DELETE method + request = urllib.request.Request( + headers=header_collection, method=method, url=request_url + ) + + response = urllib.request.urlopen(request) + response_data = {} + if parse_response: + response_data = json.load(response) + + response.close() + + return response_data + + +def workflow_run_list(auth_token, owner_repo_name, workflow_id): + request_page = 1 + while True: + data = github_request( + auth_token, + f"repos/{owner_repo_name}/actions/workflows/{urllib.parse.quote(workflow_id)}/runs", + parameter_collection={"page": request_page}, + ) + + run_list = data["workflow_runs"] + if len(run_list) < 1: + # no more items + break + + for item in run_list: + print(f"Found run ID: {item['id']}") + yield item["id"] + + # move to next page + request_page += 1 + + +def workflow_run_delete(auth_token, owner_repo_name, run_id): + github_request( + auth_token, + f"repos/{owner_repo_name}/actions/runs/{run_id}", + method="DELETE", + parse_response=False, + ) + + +def main(): + # fetch GitHub access token + auth_token = os.environ["AUTH_TOKEN"] + + # fetch requested repository and workflow ID to remove prior runs from + parser = argparse.ArgumentParser() + parser.add_argument("--repository-name", required=True) + parser.add_argument("--workflow-id", required=True) + arg_list = parser.parse_args() + + # fetch run id list from repository workflow + run_id_list = list( + workflow_run_list(auth_token, arg_list.repository_name, arg_list.workflow_id) + ) + + for run_id in run_id_list: + print(f"Deleting run ID: {run_id}") + workflow_run_delete(auth_token, arg_list.repository_name, run_id) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/third_party/SimpleBLE/utils/format.sh b/third_party/SimpleBLE/utils/format.sh new file mode 100644 index 000000000..4a8c3ebc5 --- /dev/null +++ b/third_party/SimpleBLE/utils/format.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +# Script to run clang-format locally and print differences using colordiff +# only for files that have changes. +# +# Dry run: +# >./format.sh +# +# Apply changes: +# >./format.sh apply + + +CLANG_FORMAT="clang-format" + +# Check if colordiff is installed +if ! command -v colordiff &> /dev/null; then + echo "colordiff is required but not installed. Please install it and run the script again." + exit 1 +fi + + +APPLY=false +# Check if the 'apply' argument is provided +if [ -n "$1" ]; then + if [ "$1" == "apply" ]; then + APPLY=true + else + # If the first argument is not 'apply', print usage and exit + echo "Invalid argument. Usage:" + echo "format.sh apply" + exit 1 + fi +fi + + +if [ "$1" == "apply" ]; then + APPLY=true +fi + +# Run clang-format and print differences only for files that have changes +echo "Running clang-format..." + +# Excluded paths +EXCLUDED_PATHS=( + "./simplepyble/*" + "*CMakeFiles*" + "*_deps*" + "*build*" + "*external*" +) + +FIND_CMD="find . -type f \( -name \"*.h\" -o -name \"*.hpp\" -o -name \"*.cpp\" -o -name \"*.c\" \)" +for path in "${EXCLUDED_PATHS[@]}"; do + FIND_CMD+=" ! -path \"$path\"" +done + +# Find all relevant source files, excluding specified paths, and process each file +eval "$FIND_CMD" | while read -r file; do + + # Get the differences between the original file and the formatted file + diff_output=$($CLANG_FORMAT "$file" | diff -u "$file" - | colordiff) + + # If differences are found, print the file name and the differences + if [ -n "$diff_output" ]; then + if [ "$APPLY" == true ]; then + $CLANG_FORMAT -i "$file" + echo "Applied changes to $file" + else + echo "Differences found in $file:" + echo "$diff_output" + fi + fi +done + +echo "Format completed."