mirror of
https://github.com/markqvist/Sideband.git
synced 2026-01-14 07:00:58 -05:00
Compare commits
No commits in common. "main" and "1.5.0" have entirely different histories.
197 changed files with 18116 additions and 12942 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -33,7 +33,3 @@ dist
|
|||
docs/build
|
||||
sideband*.egg-info
|
||||
sbapp*.egg-info
|
||||
LXST
|
||||
environment
|
||||
archived_build_tools
|
||||
.gradle
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
liberapay: Reticulum
|
||||
ko_fi: markqvist
|
||||
custom: "https://unsigned.io/donate"
|
||||
33
MIRROR.md
33
MIRROR.md
|
|
@ -1,33 +0,0 @@
|
|||
This repository is a public mirror. All potential future development is happening elsewhere.
|
||||
|
||||
I am stepping back from all public-facing interaction with this project. Reticulum has always been primarily my work, and continuing in the current public, internet-facing model is no longer sustainable.
|
||||
|
||||
The software remains available for use as-is. Occasional updates may appear at unpredictable intervals, but there will be no support, no responses to issues, no discussions, and no community management in this or any other public venue. If it doesn't work for you, it doesn't work. That is the entire extent of available troubleshooting assistance I can offer you.
|
||||
|
||||
If you've followed this project for a while, you already know what this means. You know who designed, wrote and tested this, and you know how many years of my life it took. You'll also know about both my particular challenges and strengths, and how I believe anything worth building needs to be built and maintained with our own hands.
|
||||
|
||||
Seven months ago, I said I needed to step back, that I was exhausted, and that I needed to recover. I believed a public resolve would be enough to effectuate that, but while striving to get just a few more useful features and protocols out, the unproductive requests and demands also ramped up, and I got pulled back into the same patterns and draining interactions that I'd explicitly said I couldn't sustain anymore.
|
||||
|
||||
So here's what you might have already guessed: I'm done playing the game by rules I can't win at.
|
||||
|
||||
Everything you need is right here, and by any sensible measure, it's done. Anyone who wants to invest the time, skill and persistence can build on it, or completely re-imagine it with different priorities. That was always the point.
|
||||
|
||||
The people who actually contributed - you know who you are, and you know I mean it when I say: Thank you. All of you who've used this to build something real - that was the goal, and you did it without needing me to hold your hand.
|
||||
|
||||
The rest of you: You have what you need. Use it or don't. I am not going to be the person who explains it to you anymore.
|
||||
|
||||
This is not a temporary break. It's not "see you after some rest", but a recognition that the current model is fundamentally incompatible with my life, my health, and my reality.
|
||||
|
||||
If you want to support continued work, you can do so at the donation links listed in this repository. But please understand, that this is not purchasing support or guaranteeing updates. It is support for work that happens on my timeline, according to my capacity, which at the moment is not what it was.
|
||||
|
||||
If you want Reticulum to continue evolving, you have the power to make that happen. The protocol is public domain. The code is open source. Everything you need is right here. I've provided the tools, but building what comes next is not my responsibility anymore. It's yours.
|
||||
|
||||
To the small group of people who has actually been here, and understood what this work was and what it cost - you already know where to find me if it actually matters.
|
||||
|
||||
To everyone else: This is where we part ways. No hard feelings. It's just time.
|
||||
|
||||
---
|
||||
|
||||
असतो मा सद्गमय
|
||||
तमसो मा ज्योतिर्गमय
|
||||
मृत्योर्मा अमृतं गमय
|
||||
61
Makefile
61
Makefile
|
|
@ -1,5 +1,3 @@
|
|||
include environment
|
||||
|
||||
devapk:
|
||||
make -C sbapp devapk
|
||||
|
||||
|
|
@ -26,64 +24,15 @@ cleanbuildozer:
|
|||
|
||||
cleanall: clean cleanbuildozer
|
||||
|
||||
remove_symlinks:
|
||||
@echo Removing symlinks for build...
|
||||
-rm ./RNS
|
||||
-rm ./LXST
|
||||
-rm ./LXMF
|
||||
|
||||
create_symlinks:
|
||||
@echo Creating symlinks...
|
||||
-ln -s ../Reticulum/RNS ./RNS
|
||||
-ln -s ../LXST/LXST ./LXST
|
||||
-ln -s ../LXMF/LXMF ./LXMF
|
||||
|
||||
preparewheel:
|
||||
pyclean .
|
||||
$(MAKE) -C sbapp cleanrns
|
||||
|
||||
compile_wheel:
|
||||
python3 setup.py bdist_wheel
|
||||
build_wheel:
|
||||
python3 setup.py sdist bdist_wheel
|
||||
|
||||
compile_sourcepkg:
|
||||
python3 setup.py sdist
|
||||
|
||||
update_share:
|
||||
$(MAKE) -C sbapp fetchshare
|
||||
|
||||
build_wheel: remove_symlinks update_share compile_wheel create_symlinks
|
||||
|
||||
build_spkg: remove_symlinks update_share compile_sourcepkg create_symlinks
|
||||
|
||||
prepare_win_pkg: clean build_spkg
|
||||
-rm -r build/winpkg
|
||||
mkdir -p build/winpkg
|
||||
LC_ALL=C $(MAKE) -C ../Reticulum clean build_spkg
|
||||
cp ../Reticulum/dist/rns-*.*.*.tar.gz build/winpkg
|
||||
cd build/winpkg; tar -zxf rns-*.*.*.tar.gz
|
||||
mv build/winpkg/rns-*.*.*/RNS build/winpkg; rm -r build/winpkg/rns-*.*.*
|
||||
LC_ALL=C $(MAKE) -C ../LXMF clean build_spkg
|
||||
cp ../LXMF/dist/lxmf-*.*.*.tar.gz build/winpkg
|
||||
cd build/winpkg; tar -zxf lxmf-*.*.*.tar.gz
|
||||
mv build/winpkg/lxmf-*.*.*/LXMF build/winpkg; rm -r build/winpkg/lxmf-*.*.*
|
||||
LC_ALL=C $(MAKE) -C ../LXST clean build_spkg
|
||||
cp ../LXST/dist/lxst-*.*.*.tar.gz build/winpkg
|
||||
cd build/winpkg; tar -zxf lxst-*.*.*.tar.gz
|
||||
mv build/winpkg/lxst-*.*.*/LXST build/winpkg; rm -r build/winpkg/lxst-*.*.*
|
||||
rm build/winpkg/LXST/filterlib*.so
|
||||
cp dist/sbapp-*.*.*.tar.gz build/winpkg
|
||||
cd build/winpkg; tar -zxf sbapp-*.*.*.tar.gz
|
||||
mv build/winpkg/sbapp-*.*.*/* build/winpkg; rm -r build/winpkg/sbapp-*.*.*
|
||||
rm build/winpkg/LXST/Codecs/libs/pyogg/libs/macos -r
|
||||
rm build/winpkg/sbapp/Makefile
|
||||
rm build/winpkg/sbapp/buildozer.spec
|
||||
cp winbuild.bat build/
|
||||
mv build/winpkg build/sideband_sources
|
||||
cd build; zip -r winbuild.zip sideband_sources winbuild.bat
|
||||
mv build/winbuild.zip dist/winbuild.zip
|
||||
|
||||
build_winexe: prepare_win_pkg
|
||||
cp dist/winbuild.zip $(WINDOWS_BUILD_TARGET)
|
||||
build_win_exe:
|
||||
python -m PyInstaller sideband.spec --noconfirm
|
||||
|
||||
release: build_wheel apk fetchapk
|
||||
|
||||
|
|
@ -91,4 +40,4 @@ upload:
|
|||
@echo Ready to publish release, hit enter to continue
|
||||
@read VOID
|
||||
@echo Uploading to PyPi...
|
||||
twine upload dist/sbapp-*
|
||||
twine upload dist/sbapp-*
|
||||
197
README.md
197
README.md
|
|
@ -1,11 +1,7 @@
|
|||
Sideband <img align="right" src="https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg"/>
|
||||
=========
|
||||
|
||||
*This repository is [a public mirror](./MIRROR.md). All development is happening elsewhere.*
|
||||
|
||||
To understand the foundational philosophy and goals of this system, read the [Zen of Reticulum](Zen%20of%20Reticulum.md).
|
||||
|
||||
Sideband is an extensible LXMF messaging and LXST telephony client, situational awareness tracker and remote control and monitoring system for Android, Linux, macOS and Windows. It allows you to communicate with other people or LXMF-compatible systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, Encrypted QR Paper Messages, or anything else Reticulum supports.
|
||||
Sideband is an extensible LXMF messaging client, situational awareness tracker and remote control and monitoring system for Android, Linux, macOS and Windows. It allows you to communicate with other people or LXMF-compatible systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, Encrypted QR Paper Messages, or anything else Reticulum supports.
|
||||
|
||||

|
||||
|
||||
|
|
@ -17,11 +13,10 @@ This also means that Sideband operates differently than what you might be used t
|
|||
|
||||
Sideband provides many useful and interesting functions, such as:
|
||||
|
||||
- **Secure** and **self-sovereign** messaging and voice calls using the LXMF and LXST protocols over Reticulum.
|
||||
- **Secure** and **self-sovereign** messaging using the LXMF protocol over Reticulum.
|
||||
- **Image** and **file transfers** over all supported mediums.
|
||||
- **Audio messages** that work even over **LoRa** and **radio links**, thanks to [Codec2](https://github.com/drowe67/codec2/) and [Opus](https://github.com/xiph/opus) encoding.
|
||||
- Secure and direct P2P **telemetry and location sharing**. No third parties or servers ever have your data.
|
||||
- The telemetry system is **completely extensible** via [simple plugins](#creating-plugins).
|
||||
- Situation display on both online and **locally stored offline maps**.
|
||||
- Geospatial awareness calculations.
|
||||
- Exchanging messages through **encrypted QR-codes on paper**, or through messages embedded directly in **lxm://** links.
|
||||
|
|
@ -44,68 +39,52 @@ Sideband can run on most computing devices, but installation methods vary by dev
|
|||
|
||||
## On Android
|
||||
|
||||
For your Android devices, you can download an [APK on the latest release page](https://github.com/markqvist/Sideband/releases/latest).
|
||||
For your Android devices, you can install Sideband through F-Droid, by adding the [Between the Borders Repo](https://reticulum.betweentheborders.com/fdroid/repo/), or you can download an [APK on the latest release page](https://github.com/markqvist/Sideband/releases/latest). Both sources are signed with the same release keys, and can be used interchangably.
|
||||
|
||||
After the application is installed on your Android device, it is also possible to pull updates directly through the **Repository** section of the application.
|
||||
|
||||
The Sideband APK file is always signed with a consistent signing certificate directly at build time. Before installing it for the first time, you can verify that the APK has not been modified, by checking that the APK file's signing certificate matches these hashes:
|
||||
|
||||
```text
|
||||
SHA-256 digest: 1c65f01f586a2b73ac4eb8bf48730b3899d046447185fd9d005685a4af20cdea
|
||||
SHA-1 digest: 4ab9269c320c72f4e4057ec7ea5acade320c2a48
|
||||
MD5 digest: 09afff8c505089a544ad2bf371c29422
|
||||
```
|
||||
|
||||
Sideband will never be released on app store platforms that does not support complete control of the APK signing directly from the developer. If you download Sideband from any other source than this repository, and the certificate hashes do not match, **do not install it**.
|
||||
|
||||
The Android version of Sideband has been carefully set up to **not** use any Android APIs or functionality that are dependent on Google (or other vendor-specific) components or libraries. It uses only raw Android OS APIs, and accesses them directly, instead of through "compatibility", "support" or "helper" libraries, which can often hijack application data flow into privacy-compromising pipelines controlled by Google or other vendors.
|
||||
|
||||
This also means that Sideband is designed to be fully compatible with custom (and more privacy-friendly) Android versions and ROMs, such as GrapheneOS, de-googled devices and other custom ROMs.
|
||||
|
||||
## On Linux
|
||||
|
||||
On all Linux-based operating systems, Sideband is available as a `pip`/`pipx` package. You can either pull and install Sideband directly from the `pip` repository, or download and install locally using `whl` package from the [latest release page](https://github.com/markqvist/Sideband/releases/latest).
|
||||
On all Linux-based operating systems, Sideband is available as a `pipx`/`pip` package. This installation method **includes desktop integration**, so that Sideband will show up in your applications menu and launchers. Below are install steps for the most common recent Linux distros. For Debian 11, see the end of this section.
|
||||
|
||||
This installation method **includes desktop integration**, so that Sideband will show up in your applications menu and launchers. Below are install steps for the most common recent Linux distros. For Debian 11, see the end of this section.
|
||||
**Please note!** The very latest Python release, Python 3.13 is currently **not** compatible with the Kivy framework, that Sideband uses to render its user interface. If your Linux distribution uses Python 3.13 as its default Python installation, you will need to install an earlier version as well. Using [the latest release of Python 3.12](https://www.python.org/downloads/release/python-3127/) is recommended.
|
||||
|
||||
#### Basic Installation
|
||||
You will first need to install a few dependencies for voice calls, audio messaging and Codec2 support to work:
|
||||
You will first need to install a few dependencies for audio messaging and Codec2 support to work:
|
||||
|
||||
```bash
|
||||
# For Debian 13+, Ubuntu 24.04+ and derivatives
|
||||
sudo apt install python3-pip python3-pyaudio libopusfile0 codec2 xclip xsel
|
||||
|
||||
# For Debian 12+, Ubuntu 22.04+ and derivatives
|
||||
sudo apt install python3-pip python3-pyaudio python3-dev build-essential libopusfile0 portaudio19-dev codec2 xclip xsel
|
||||
# For Debian (12+), Ubuntu (22.04+) and derivatives
|
||||
sudo apt install pipx python3-pyaudio python3-dev build-essential libopusfile0 portaudio19-dev codec2 xclip xsel
|
||||
|
||||
# For Manjaro and derivatives
|
||||
pamac install python-pyaudio opusfile codec2 xclip xsel
|
||||
pamac install python-pipx python-pyaudio base-devel codec2 xclip xsel
|
||||
|
||||
# For Arch and derivatives
|
||||
sudo pacman -Sy python-pyaudio opusfile codec2 xclip xsel
|
||||
sudo pacman -Sy python-pipx python-pyaudio base-devel codec2 xclip xsel
|
||||
|
||||
```
|
||||
|
||||
Once those are installed, install the Sideband application itself:
|
||||
|
||||
```bash
|
||||
# Finally, install Sideband using pip:
|
||||
pip install sbapp --break-system-packages
|
||||
# Finally, install Sideband using pipx:
|
||||
pipx install sbapp
|
||||
|
||||
# If Sideband does not show up in your application menu
|
||||
# or as an executable command in your terminal, reboot
|
||||
# your computer.
|
||||
sudo reboot
|
||||
# If you need to specify a specific Python version,
|
||||
# use something like the following:
|
||||
pipx install sbapp --python python3.12
|
||||
```
|
||||
|
||||
After installation, you can now run Sideband in a number of different ways:
|
||||
|
||||
```bash
|
||||
# If this is the first time installing something with pipx,
|
||||
# you may need to use the following command, to make your
|
||||
# installed applications available. You'll probably need
|
||||
# to close and reopen your terminal after this.
|
||||
pipx ensurepath
|
||||
|
||||
# The first time you run Sideband, you will need to do it
|
||||
# from the terminal, for the application launcher item to
|
||||
# show up. On some distros you may also need to log out
|
||||
# and back in again, or simply reboot the machine for the
|
||||
# application entry to show up in your menu.
|
||||
# from the terminal:
|
||||
sideband
|
||||
|
||||
# At the first launch, it will add an application icon
|
||||
|
|
@ -122,18 +101,18 @@ sideband --daemon
|
|||
sideband -v
|
||||
```
|
||||
|
||||
If you do not already have Reticulum connectivity set up on your computer or local network, you will probably want to edit the Reticulum configuration file at `~/.reticulum/config` and [add any interfaces](https://reticulum.network/manual/interfaces.html) you need for connectivity.
|
||||
|
||||
#### Advanced Installation
|
||||
You can also install Sideband in various alternative ways:
|
||||
|
||||
```bash
|
||||
# Install Sideband via pipx instead of pip:
|
||||
pipx install sbapp
|
||||
# Install Sideband via pip instead of pipx:
|
||||
pip install sbapp
|
||||
|
||||
# Or, if pip is externally managed:
|
||||
pip install sbapp --break-system-packages
|
||||
|
||||
# Or, if you intend to run Sideband in headless
|
||||
# daemon or console mode, you can also install
|
||||
# it without any of the normal UI dependencies:
|
||||
# daemon mode, you can also install it without
|
||||
# any of the normal UI dependencies:
|
||||
pip install sbapp --no-dependencies
|
||||
|
||||
# In the case of using --no-dependencies, you
|
||||
|
|
@ -141,9 +120,6 @@ pip install sbapp --no-dependencies
|
|||
# and LXMF dependencies:
|
||||
pip install rns lxmf
|
||||
|
||||
# Dependencies can vary slightly on older OS
|
||||
# versions, due to different package names.
|
||||
|
||||
# Install Sideband on Debian 11 and derivatives:
|
||||
sudo apt install python3-pip python3-pyaudio python3-dev build-essential libopusfile0 portaudio19-dev codec2 xclip xsel
|
||||
pip install sbapp
|
||||
|
|
@ -155,42 +131,34 @@ python3 -m sbapp.main
|
|||
|
||||
## On Raspberry Pi
|
||||
|
||||
You can install Sideband on all Raspberry Pi models that support 64-bit operating systems, and can run at least Python version 3.11.
|
||||
You can install Sideband on all Raspberry Pi models that support 64-bit operating systems, and can run at least Python version 3.11. Since some of Sideband's dependencies don't have pre-built packages ready for 64-bit ARM processors yet, you'll need to install a few extra packages, that will allow building these while installing.
|
||||
|
||||
The install instructions below assume that you are installing Sideband on 64-bit Raspberry Pi OS (based on Debian 13 "Trixie" or later). If you're running something else on your Pi, you might need to modify some commands slightly.
|
||||
Aditionally, the `pycodec2` package needs to be installed manually. I have provided a pre-built version, that you can download and install with a single command, or if you don't want to trust my pre-built version, you can [build and install it from source yourself](https://github.com/gregorias/pycodec2/blob/main/DEV.md).
|
||||
|
||||
To install Sideband on Raspberry Pi with full support for voice calls, audio messages and Codec2, follow these steps:
|
||||
The install instructions below assume that you are installing Sideband on 64-bit Raspberry Pi OS (based on Debian Bookworm). If you're running something else on your Pi, you might need to modify some commands slightly. To install Sideband on Raspberry Pi, follow these steps:
|
||||
|
||||
```bash
|
||||
# First of all, install the dependencies required
|
||||
# for audio processing and voice calls.
|
||||
#
|
||||
# On Raspberry Pi OS "Trixie" (based on Debian 13)
|
||||
# or newer, install these packages:
|
||||
sudo apt install python3-pyaudio codec2 xclip xsel
|
||||
# First of all, install the required dependencies:
|
||||
sudo apt install python3-pip python3-pyaudio python3-dev python3-cryptography build-essential libopusfile0 libsdl2-dev libavcodec-dev libavdevice-dev libavfilter-dev portaudio19-dev codec2 libcodec2-1.0 xclip xsel
|
||||
|
||||
# If you don't want to compile pycodec2 yourself,
|
||||
# download the pre-compiled package provided here
|
||||
wget https://raw.githubusercontent.com/markqvist/Sideband/main/docs/utilities/pycodec2-3.0.1-cp311-cp311-linux_aarch64.whl
|
||||
|
||||
# Install it:
|
||||
pip install ./pycodec2-3.0.1-cp311-cp311-linux_aarch64.whl --break-system-packages
|
||||
|
||||
# You can now install Sideband
|
||||
pip install sbapp --break-system-packages
|
||||
|
||||
# Restart your Raspberry Pi to ensure the program
|
||||
# is available in your PATH and application menus:
|
||||
# Restart your Raspberry Pi
|
||||
sudo reboot
|
||||
|
||||
# Everything is ready! You can now run Sideband
|
||||
# from the terminal, or from the application menu:
|
||||
# from the terminal, or from the application menu
|
||||
sideband
|
||||
```
|
||||
|
||||
If you're using an older version of Raspberry Pi OS, you will need to install these dependencies instead, before installing Sideband:
|
||||
|
||||
```bash
|
||||
# Package dependencies for Raspberry Pi OS based
|
||||
# on Debian 12 Bookworm
|
||||
sudo apt install python3-pip python3-pyaudio python3-dev python3-cryptography build-essential libopusfile0 libsdl2-dev libavcodec-dev libavdevice-dev libavfilter-dev portaudio19-dev codec2 libcodec2-1.0 xclip xsel
|
||||
```
|
||||
|
||||
If you do not already have Reticulum connectivity set up on your computer or local network, you will probably want to edit the Reticulum configuration file at `~/.reticulum/config` and [add any interfaces](https://reticulum.network/manual/interfaces.html) you need for connectivity.
|
||||
|
||||
## On macOS
|
||||
|
||||
To install Sideband on macOS, you have two options available:
|
||||
|
|
@ -208,38 +176,26 @@ If you install Sideband from the DMG file, it is still recommended to install th
|
|||
|
||||
```bash
|
||||
# Install Reticulum and utilities with pip:
|
||||
pip3 install rns --user
|
||||
pip3 install rns
|
||||
|
||||
# On some versions, you may need to use the
|
||||
# flag --break-system-packages to install:
|
||||
pip3 install rns --user --break-system-packages
|
||||
pip3 install rns --break-system-packages
|
||||
```
|
||||
|
||||
If you do not have Python and `pip` available, [download and install it](https://www.python.org/downloads/) first.
|
||||
|
||||
If you do not already have Reticulum connectivity set up on your computer or local network, you will probably want to edit the Reticulum configuration file at `~/.reticulum/config` and [add any interfaces](https://reticulum.network/manual/interfaces.html) you need for connectivity.
|
||||
|
||||
#### Source Package Install
|
||||
|
||||
For more advanced setups, including the ability to run Sideband in headless daemon mode, enable debug logging output, configuration import and export and more, you may want to install it from the source package via `pip` instead.
|
||||
|
||||
**Please note!** The very latest Python release, Python 3.13 is currently **not** compatible with the Kivy framework, that Sideband uses to render its user interface. If your version of macOS uses Python 3.13 as its default Python installation, you will need to install an earlier version as well. Using [the latest release of Python 3.12](https://www.python.org/downloads/release/python-3127/) is recommended.
|
||||
|
||||
To install Sideband via `pip`, follow these instructions:
|
||||
|
||||
```bash
|
||||
# Install Sideband and dependencies on macOS using pip:
|
||||
pip3 install sbapp --user
|
||||
|
||||
# Or, if your Python environment is externally managed:
|
||||
pip3 install sbapp --user --break-system-packages
|
||||
|
||||
# Important note! On some macOS versions, programs that
|
||||
# were installed with pip does not get added to your
|
||||
# PATH environment variable. If this is the case, pip
|
||||
# will print out a warning about it during installation.
|
||||
# In that case, you will have to manually add the path
|
||||
# shown in the installation warning to your PATH, before
|
||||
# you can run commands such as "sideband" or "rnstatus"
|
||||
# from your shell.
|
||||
pip3 install sbapp
|
||||
|
||||
# Run Sideband from the terminal:
|
||||
#################################
|
||||
|
|
@ -282,8 +238,6 @@ Simply download the packaged Windows ZIP file from the [latest release page](htt
|
|||
|
||||
When running Sideband for the first time, a default Reticulum configuration file will be created, if you don't already have one. If you don't have any existing Reticulum connectivity available locally, you may want to edit the file, located at `C:\Users\USERNAME\.reticulum\config` and manually add an interface that provides connectivity to a wider network. If you just want to connect over the Internet, you can add one of the public hubs on the [Reticulum Testnet](https://reticulum.network/connect.html).
|
||||
|
||||
#### Installing Reticulum Utilities
|
||||
|
||||
Though the ZIP file contains everything necessary to run Sideband, it is also recommended to install the Reticulum command line utilities separately, so that you can use commands like `rnstatus` and `rnsd` from the command line. This will make it easier to manage Reticulum connectivity on your system. If you do not already have Python installed on your system, [download and install it](https://www.python.org/downloads/) first.
|
||||
|
||||
**Important!** When asked by the installer, make sure to add the Python program to your `PATH` environment variables. If you don't do this, you will not be able to use the `pip` installer, or run any of the installed commands. When Python has been installed, you can open a command prompt and install the Reticulum package via `pip`:
|
||||
|
|
@ -308,28 +262,6 @@ The Sideband application can now be launched by running the command `sideband` i
|
|||
|
||||
Since this installation method automatically installs the `rns` and `lxmf` packages as well, you will also have access to using all the included RNS and LXMF utilities like `rnstatus`, `rnsd` and `lxmd` on your system.
|
||||
|
||||
# Creating Plugins
|
||||
|
||||
Sideband features a flexible and extensible plugin system, that allows you to hook all kinds of control, status reporting, command execution and telemetry collection into the LXMF messaging system. Plugins can be created as either *Telemetry*, *Command* or *Service* plugins, for different use-cases.
|
||||
|
||||
To create plugins for Sideband, you can find a variety of [code examples](https://github.com/markqvist/Sideband/tree/main/docs/example_plugins) in this repository, that you can use as a basis for writing your own plugins. The example plugins include:
|
||||
|
||||
- [Custom telemetry](https://github.com/markqvist/Sideband/blob/main/docs/example_plugins/telemetry.py)
|
||||
- [Getting BME280 temperature, humidity and pressure](https://github.com/markqvist/Sideband/blob/main/docs/example_plugins/bme280_telemetry.py)
|
||||
- [Basic commands](https://github.com/markqvist/Sideband/blob/main/docs/example_plugins/basic.py)
|
||||
- [Location telemetry from GPSd on Linux](https://github.com/markqvist/Sideband/blob/main/docs/example_plugins/gpsd_location.py)
|
||||
- [Location telemetry from Windows Location Provider](https://github.com/markqvist/Sideband/blob/main/docs/example_plugins/windows_location.py)
|
||||
- [Getting statistics from your LXMF propagation node](https://github.com/markqvist/Sideband/blob/main/docs/example_plugins/lxmd_telemetry.py)
|
||||
- [Viewing cameras and streams](https://github.com/markqvist/Sideband/blob/main/docs/example_plugins/view.py)
|
||||
- [Fetching an XKCD comic](https://github.com/markqvist/Sideband/blob/main/docs/example_plugins/comic.py)
|
||||
- [Creating a service plugin](https://github.com/markqvist/Sideband/blob/main/docs/example_plugins/service.py)
|
||||
|
||||
For creating telemetry plugins, Sideband includes 20+ built-in sensor types to chose from, for representing all kinds telemetry data. If none of those fit your needs, there is a `Custom` sensor type, that can include any kind of data.
|
||||
|
||||
Command plugins allow you to define any kind of action or command to be run when receiving command messages from other LXMF clients. In the example directory, you will find various command plugin templates, for example for viewing security cameras or webcams through Sideband.
|
||||
|
||||
Service plugins allow you to integrate any kind of service, bridge or other system into Sideband, and have that react to events or state changes in Sideband itself.
|
||||
|
||||
# Paper Messaging Example
|
||||
|
||||
You can try out the paper messaging functionality by using the following QR-code. It is a paper message sent to the LXMF address `6b3362bd2c1dbf87b66a85f79a8d8c75`. To be able to decrypt and read the message, you will need to import the following base32-encoded Reticulum Identity into the app:
|
||||
|
|
@ -353,21 +285,38 @@ You can help support the continued development of open, free and private communi
|
|||
```
|
||||
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
|
||||
```
|
||||
- Bitcoin
|
||||
```
|
||||
bc1p4a6axuvl7n9hpapfj8sv5reqj8kz6uxa67d5en70vzrttj0fmcusgxsfk5
|
||||
```
|
||||
- Ethereum
|
||||
```
|
||||
0xae89F3B94fC4AD6563F0864a55F9a697a90261ff
|
||||
0xFDabC71AC4c0C78C95aDDDe3B4FA19d6273c5E73
|
||||
```
|
||||
- Bitcoin
|
||||
```
|
||||
35G9uWVzrpJJibzUwpNUQGQNFzLirhrYAH
|
||||
```
|
||||
- Liberapay: https://liberapay.com/Reticulum/
|
||||
|
||||
- Ko-Fi: https://ko-fi.com/markqvist
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
# Planned Features
|
||||
|
||||
- <s>Secure and private location and telemetry sharing</s>
|
||||
- <s>Including images in messages</s>
|
||||
- <s>Sending file attachments</s>
|
||||
- <s>Offline and online maps</s>
|
||||
- <s>Paper messages</s>
|
||||
- <s>Using Sideband as a Reticulum Transport Instance</s>
|
||||
- <s>Encryption keys export and import</s>
|
||||
- <s>Plugin support for commands, services and telemetry</s>
|
||||
- <s>Sending voice messages (using Codec2 and Opus)</s>
|
||||
- <s>Adding a Linux desktop integration</s>
|
||||
- <s>Adding prebuilt Windows binaries to the releases</s>
|
||||
- <s>Adding prebuilt macOS binaries to the releases</s>
|
||||
- <s>A debug log viewer</s>
|
||||
- Adding a Nomad Net page browser
|
||||
- LXMF sneakernet functionality
|
||||
- Network visualisation and test tools
|
||||
- Better message sorting mechanism
|
||||
|
||||
# License
|
||||
Unless otherwise noted, this work is licensed under a [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License][cc-by-nc-sa].
|
||||
|
||||
|
|
|
|||
|
|
@ -1,415 +0,0 @@
|
|||
# Zen of Reticulum
|
||||
|
||||
## I: The Illusion Of The Center
|
||||
|
||||
For the better part of a generation, we have been taught to visualize the digital world through the lens of hierarchy. The mental maps we carry are dominated by a single, misleading image: **The Cloud**.
|
||||
|
||||
We imagine the network as a vast, ethereal space "up there" or "out there". A centralized repository of services and data to which we, the lowly clients, must connect. We build our software with this assumption hardcoded into our logic: *There is a server. The server has the authority. The server knows the way. I must find the server to function*.
|
||||
|
||||
This is the Client-Server mental model, and it is the primary obstacle to understanding Reticulum.
|
||||
|
||||
### Fallacy Of The Cloud
|
||||
|
||||
The first step in the Zen of Reticulum is to realize that *there is no cloud*. There is only other people's computers. When you build for the cloud, you are building *for* a landlord. You are accepting that your application's existence is conditional on the permission, uptime, and continued goodwill of a central authority.
|
||||
|
||||
In Reticulum, you must shift your thinking from "connecting to" to "being among". Reticulum is not a service you subscribe to - *it is a fabric you inhabit*. There is no "up there". There is only *here* and *there*, and the space between them is peer-to-peer.
|
||||
|
||||
### Decentralization Or Uncentralizability?
|
||||
|
||||
It is common to hear the word "decentralized" thrown around in modern tech circles. But often, this is merely a marketing term for "slightly distributed centralization". A blockchain with a few dominant miners, or a federated protocol with a few giant servers. *In practice*, it's still centralized. It simply has a few centers instead of one.
|
||||
|
||||
Reticulum goes further. It wants **Uncentralizability**.
|
||||
|
||||
This is not a wishful political stance, but a foundational mathematical characteristic of the protocol, onto which everything else has been built. Reticulum assumes that every peer on the network is potentially hostile, and every link is potentially compromised. It is designed with no "privileged" nodes. While some nodes may act as Transport Instances - forwarding traffic for others - they do so *blindly*, and they only know about their immediate surroundings, and nothing more. They route based on cryptographic proofs, not on administrative privilege. They cannot see who is talking to whom, nor can they selectively manipulate traffic without breaking their own ability to route entirely.
|
||||
|
||||
The system is designed to make hierarchy structurally impossible. You cannot hijack an address, because there is no central registry to hijack. You cannot block a user, because there is no central switch to flip. You can offer paths through the network, but you can't force anyone to use them.
|
||||
|
||||
### Death To The Address
|
||||
|
||||
To break free of the center, you must also let go of the concept of the "Address".
|
||||
|
||||
In the IP world, an address is a location. It is a coordinate in a *deeply hierarchical* and static grid. If you move your computer to a different house, your address changes. If your router reboots, your address might change. Your *identity* is bound to your *location*, and therefore, it is fragile, and easily controlled.
|
||||
|
||||
Reticulum abolishes this link between *Identity* and *Location*.
|
||||
|
||||
In Reticulum, an address is not a place; it is a **Hash of an Identity**. It is a cryptographic representation of *who* you are, not *where* you are. Because of this, your address is portable. You can take a laptop from a WiFi cafe in Berlin, to a LoRa mesh in the mountains, to a packet radio link on a boat, and your "address" - your *Destination Hash* - never changes.
|
||||
|
||||
The network does not route to a place; it routes to a *person* (or a machine). When you send a packet, you are not targeting a coordinate in a grid; you are encrypting a message for a specific entity. The network dynamically discovers where that entity currently resides, and it does so in a way where no one really knows where that entity is actually located physically.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** *"I am at `192.168.1.5`. Come find me"*.
|
||||
- **The Zen Way:** *"I am `<327c1b2f87c9353e01769b01090b18f2>`. Wherever I am, my peers can reach me"*.
|
||||
|
||||
Once you stop thinking about servers and start thinking about portable identities, where everyone can always reach everyone else directly, the illusion of the center fades away. You realize there *is* no center holding the network together. No coordinators or bureaucrats required. The network is simply the sum of its peers, communicating directly, sovereignly, and without a master.
|
||||
|
||||
|
||||
## II: Physics Of Trust
|
||||
*Paranoia Is A Great Design Principle*
|
||||
|
||||
If we accept that there is no center - that the network is a chaotic, peer-to-peer mesh - we are forced to confront a terrifying reality: **There is no one guarding the door**.
|
||||
|
||||
In the traditional networking mindset, we rely on the concept of the "trusted core". We assume our local coffee shop WiFi is safe, or that the backbone providers are neutral custodians. We build our security like a castle: strong walls on the outside, soft and trusting on the inside. We use encryption only when we step out into the "wild" internet.
|
||||
|
||||
### Hostile Environments
|
||||
|
||||
The Zen of Reticulum requires you to invert this. You must assume that *every* environment is hostile. This isn't cynicism, just uncaring physics.
|
||||
|
||||
When you transmit information over radio waves, you are shouting into a crowded room. Anyone can listen. When you traverse the internet, your packets pass through routers controlled by strangers, corporations, and state actors. Assuming privacy in this environment without cryptographic protection is not optimism but gross negligence.
|
||||
|
||||
Reticulum is built on the premise that every link is tapped, and every peer is a potential adversary. If your system cannot survive an adversary owning the physical layer, it cannot survive at all.
|
||||
|
||||
But this is the paradox: By assuming the network is hostile, you make it safe. When you accept the dangers for what they are, they become manageable. When you stop trusting the infrastructure and start trusting the math, you eliminate the single point of failure: Human integrity.
|
||||
|
||||
### Encryption Is Not A Feature
|
||||
|
||||
In the world of TCP/IP, encryption is an afterthought. It is a layer we slap on top of the protocol (HTTPS, TLS) to patch the security holes of the original design. It is a "feature" you sometimes *enable* for "sensitive data". This is fundamentally flawed, since all data is sensitive.
|
||||
|
||||
In Reticulum, encryption is **gravity**.
|
||||
|
||||
It is not optional. It is not a plugin. It is the *fundamental force that allows the network to exist*. If you were to strip the encryption from Reticulum, the routing would break. The Transport system uses cryptographic signatures and entropy to verify paths and pass information. If packets were plaintext, intermediate nodes could not prove that a route was valid, nor could endpoints prevent spoofing or tampering.
|
||||
|
||||
In Reticulum, the entropy of the encrypted packet *is* the routing logic.
|
||||
|
||||
To ask for a version of Reticulum without encryption is like asking for a version of the ocean without liquid. You are not asking for a feature change; you're asking for a different physical universe. We design for a universe where information has mass, structure, and integrity.
|
||||
|
||||
### Zero-Trust Architectures
|
||||
|
||||
We must unlearn our reliance on **Institutional Trust**.
|
||||
|
||||
For decades, we have been trained to trust authorities. We trust a website because a chain of Certificate Authorities (companies we don't know) vouches for it. We trust an app because it is in an app store (run by a corporation we don't control). We trust a message because it comes from a phone number assigned by a telecom. Yet, everything in our digital information sphere today is more untrustworthy and risky than a medieval second-hand underwear market.
|
||||
|
||||
Reticulum replaces institutional trust with **Cryptographic Proof**.
|
||||
|
||||
In Reticulum, you do not trust a node because it has a nice hostname or because it is listed in a directory. You trust it because it holds the private key corresponding to the Destination Hash you are communicating with. This trust is binary, mathematical, and **absolute**. Either the signature matches, or it does not. There is no "maybe".
|
||||
|
||||
This shift moves the power from the institution to the individual. You become the ultimate arbiter of your own trust relationships. You decide which keys to accept, which paths to follow, and which identities to recognize.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** *"I trust this site because the browser says the lock icon is green"*.
|
||||
- **The Zen Way:** *"I trust this destination because I have verified its hash fingerprint out-of-band, and the math confirms the signature"*.
|
||||
|
||||
When you internalize the Physics of Trust, you stop looking for protection from firewalls, VPNs, and Terms of Service agreements. You realize that true security comes from the design of the protocol itself. You can stop trusting the cloud, and you start trusting the code - because you can verify it yourself.
|
||||
|
||||
|
||||
## III: Merits Of Scarcity
|
||||
*Every Bit Counts*
|
||||
|
||||
We have grown addicted to abundance. In the modern digital ecosystem, bandwidth is treated as an endless, flat ocean. We stream high-definition video without a thought, we ship entire libraries of code just to render a single button, and we measure performance in gigabits per second. This abundance has hollowed out our craft. When constraints vanish, efficiency dies, and with it, a certain kind of Clarity and Quality.
|
||||
|
||||
Reticulum asks you to step out of the ocean and onto the tightrope.
|
||||
|
||||
### The Bandwidth Fallacy
|
||||
|
||||
The Zen of Reticulum requires the realization that **5 bits per second is a valid speed**.
|
||||
|
||||
To a modern developer, this sounds like paralysis. But there is a profound freedom in limits: When you have a gigabit connection, you can be incredibly sloppy. You can be wasteful. You can push your problems onto the infrastructure. *"It’s slow? Get a faster router"*.
|
||||
|
||||
But on a high-latency, low-bandwidth link (be it a noisy HF radio channel or a tenuous LoRa hop) you cannot push problems anywhere. You must solve them. The network does not negotiate with waste.
|
||||
|
||||
This forces a shift from consumption to interaction. You are no longer, then, consuming a service provided by a fat pipe; you are engaging in a careful negotiation with the physical medium. The medium becomes a partner in the conversation, not just a dumb conduit. You suddenly need to *understand the world to be in it*.
|
||||
|
||||
### Cost Of A Byte
|
||||
|
||||
In a scarce economy, a byte is not just data, but energy, time, and space.
|
||||
|
||||
Every byte you transmit consumes battery life on a solar-powered node. It occupies valuable airtime that could have been used by another peer. It represents a measurable slice of the electromagnetic spectrum.
|
||||
|
||||
When you internalize this, you begin to write code differently. You stop asking, "How much data can I send?" and start asking, "What is the *minimum* amount of information required to convey this intent? How can I best utilize my informational entropy?"
|
||||
|
||||
This is where the elegance of Reticulum shines. The protocol is designed to strip away the non-essential. A link establishment takes three very small packets. A destination hash fits in 16 bytes. The overhead is vanishingly small, leaving almost the entire channel for the message itself.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** *"I need to send a status update. I'll send a JSON object with metadata, timestamps, and user profile info (15KB)."*
|
||||
- **The Zen Way:** *"I need to send a status update. I'll send a single byte representing the state code. The context is already known."*
|
||||
|
||||
This is of course optimization, but more importantly, *it is a form of respect*. Efficiency in a shared medium is an act of stewardship. By taking only what you need from the network, you leave room for others. The network listens to those who speak with purpose.
|
||||
|
||||
### Flow & Time
|
||||
|
||||
Scarcity also teaches us about time. We have become addicted to the *synchronous* now - the instant ping, the real-time stream. But Reticulum embraces *asynchronous* time.
|
||||
|
||||
When links are intermittent and latency is measured in minutes or hours, "real-time" is an illusion. Reticulum doesn't encourage **Store and Forward** as a mere fallback, but as a primary mode of existence. You write a message, it propagates when it can, and it arrives when it arrives.
|
||||
|
||||
This changes the psychological texture of communication. It removes the anxiety of the immediate response. It allows for contemplation. You are not demanding the recipient's attention *right now*; you are placing a gift in their path, to be found when they are ready.
|
||||
|
||||
By designing for delay, you design for resilience. You are no longer building a house of cards that collapses when a single packet drops. You are building a stone arch that distributes the load *over time*.
|
||||
|
||||
### Liberation From Limits
|
||||
|
||||
There is a strange optimism in scarcity. When you are forced to work within strict constraints, you are forced to prioritize. *You* must decide what truly matters. *That* is the real core of agency.
|
||||
|
||||
In the infinite fantasy world of The Cloud, everything is urgent, so nothing is. In the economy of Reticulum, the cost of transmission forces you to weigh the value of your message. Do you really need to send that heart beat? Is that photo essential?
|
||||
|
||||
When you strip away the noise, what remains is *signal*.
|
||||
|
||||
This discipline creates a different kind of developer. It creates a craftsman who understands that the best code is the code you don't have to write. It creates a user who understands that the most powerful message is the one that is *understood*, not the one that is loudest. In the world of Reticulum, you are not a mere consumer of bandwidth; you are an architect of intent.
|
||||
|
||||
|
||||
## IV: Sovereignty Through Infrastructure
|
||||
**Be Your Own Network**
|
||||
|
||||
We live in an era of digital tenancy. We lease our connectivity from ISPs. We rent our storage from cloud providers. We even borrow our identity from social media platforms. We are tenants in a house we did not build, governed by rules we did not write, subject to eviction at the whim of a landlord who has never met us.
|
||||
|
||||
The Zen of Reticulum is the realization that you *can* own the house.
|
||||
|
||||
### A Carrier-Grade Fallacy
|
||||
|
||||
For decades, we have been gaslit into believing that networking is really not just hard, but impossible. It is presented as a dark art reserved for telcos and billionaires, requiring millions of dollars of fiber optics, climate-controlled data centers, and armies of engineers. We are told that building reliable infrastructure is "too complex" for the individual or small organization.
|
||||
|
||||
This is a big, fat lie.
|
||||
|
||||
Physics is simple. A radio wave needs a transmitter and a receiver. A packet needs a path. The "complexity" of the modern internet is largely bureaucratic - a mountain of billing systems, regulatory capture, and legacy cruft designed to keep the gatekeepers in power.
|
||||
|
||||
Reticulum strips away the bureaucracy. It runs on hardware that costs the price of a dinner. It runs on spectrum that is free to use. It demonstrates that a robust, planetary-scale network does not require a Fortune 500 company. It requires only the will to deploy, and the distributed, uncoordinated efforts of many individuals.
|
||||
|
||||
### Personal Infrastructure
|
||||
|
||||
This is where the rubber meets the road. You can read about Reticulum, you can understand the theory, but the insights only arrive when you plug in a radio and run a Transport Node. Suddenly, you are no longer a consumer. You're an operator.
|
||||
|
||||
This shift is subtle but profound. When you run your own infrastructure, the network ceases to be a service that is provided *to* you. It becomes a space that you *inhabit*. You become responsible for the flow of information. You gain an intimate understanding of the medium - the way the weather affects the radio waves, the way the topology changes, the way the packets dance through the ether.
|
||||
|
||||
There is a quiet competence that comes from this. You stop asking "Is the internet down?" and start asking "Is *my* links up?" You stop waiting for a technician and start checking the logs. This is a form of strength. To understand the system that carries your words is to be free from the mystery that keeps you dependent.
|
||||
|
||||
### The Ability To Disconnect
|
||||
|
||||
Why go to the trouble? Why buy the radio, write the config, and leave the Pi running in the corner?
|
||||
|
||||
Because the old, centralized network is fragile. And because most of us doesn't even really want to be there anymore.
|
||||
|
||||
The internet we rely on today is a chain of single points of failure. Cut the undersea cable, and a continent goes dark. Shut down the power grid, and the cloud evaporates. Deprioritize the "wrong" traffic, and the flow of information is strangled.
|
||||
|
||||
Sovereignty is the ability to survive the cut, whether or not that cut was an accident or on purpose.
|
||||
|
||||
When you build your own infrastructure, you build a lifeline. Reticulum is designed to function over media that the traditional internet cannot touch - bare wires, battery-powered radios, ad-hoc WiFi meshes. When the grid fails, or the censors arrive, or the bill goes unpaid, your Reticulum network continues to hum.
|
||||
|
||||
This is not about "dropping out" of society. It is about building a substrate on which an actual *Society* can function.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** "My connection is slow. I should call my ISP and complain."
|
||||
- **The Zen Way:** "The path is noisy. I will adjust the antenna or find a better route."
|
||||
|
||||
By taking ownership of the infrastructure, you take ownership of your voice. You stop shouting into someone else's megaphone and start building your own. The network is no longer something that happens to you; it is something you make happen.
|
||||
|
||||
|
||||
# V: Identity and Nomadism
|
||||
**A Fluid Self**
|
||||
|
||||
In the old world, you are defined by your coordinates. If you are at `34.109.71.5`, you're *here*. If you unplug the cable and walk down the street, you vanish. Your digital self evaporates because it was tethered to the wall. You are a ghost in the endless machinations of gears, levers and transistors, bound to the hardware, and those that own it.
|
||||
|
||||
This creates a subtle, constant anxiety. We are terrified of disconnecting because, in the architecture of the old web, disconnecting is a kind of death.
|
||||
|
||||
The Zen of Reticulum offers a different way to be.
|
||||
|
||||
### Portable Existence
|
||||
|
||||
In Reticulum, your identity is not a location, or a username granted by a service. It is a cryptographic key - a complex, unique mathematical signature that exists independently of the physical world. You can carry it only in your mind, if you want to.
|
||||
|
||||
Think of it less like a street address and more like a name. *A true name*.
|
||||
|
||||
If you travel from Berlin to Tokyo, you do not change your name. You are still you. The people who know you can still recognize you. Reticulum applies this principle to the network layer. Your Destination Hash is **invariant**. It travels with you, stored securely on your device, *immutable as a stone*.
|
||||
|
||||
This changes the relationship between you and the machine. You are not "logged into" the network via a specific gateway. You *are* the endpoint. The network does not connect to a place; *it converges on you*.
|
||||
|
||||
### Roaming Nodes
|
||||
|
||||
This freedom introduces a new concept of time and space: **Nomadism**.
|
||||
|
||||
Because your identity is portable, your connectivity can be fluid. You can be sitting at a desk connected to a fiber backbone one moment, and walking through a field connected only to a long-range LoRa mesh the next. To the rest of the network, nothing has changed. Your friends do not need to update your contact info. The messages they send do not bounce back. The network senses the shift in the medium and reroutes the flow of data automatically.
|
||||
|
||||
You are no longer a stationary node in a fixed grid. You are a wanderer in a fluid medium.
|
||||
|
||||
The interfaces - whether it is WiFi, Ethernet, Packet Radio, or a physical wire - is merely the clothing your node wears. You change it to suit the environment. Underneath, you remain the same. This is the liberation of the protocol. It treats the physical medium as a transient circumstance, not a definition of self.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** *"I lost connection. I have to reconnect to the VPN to tell them where I am now."*
|
||||
- **The Zen Way:** *"I moved. The network subtly bends to accomodate this new reality."*
|
||||
|
||||
### Announcing Presence
|
||||
|
||||
How does the network find a wanderer? It listens.
|
||||
|
||||
In the IP world, we query directories. We ask a server, "Where is Mark?" The server checks its database and gives us a coordinate. This means that someone, somewhere, is keeping track of you. It assumes and *requires* surveillance.
|
||||
|
||||
Reticulum replaces surveillance with **Announces**.
|
||||
|
||||
Instead of asking a central authority where you are, you simply state your presence. You broadcast a cryptographic proof: "I am here, and I am who I say I am". This ripples out through the mesh. Your neighbors hear it, update their path tables, and pass it on.
|
||||
|
||||
This is a quiet, organic process. It is the digital equivalent of lighting lanterns in the dark. You do not need to chase the light; you let the light find you. It respects your autonomy. You choose when to announce, how often to speak, and to whom. You also choose when to disappear - for but a moment or perpetually.
|
||||
|
||||
### Anchor In The Flow
|
||||
|
||||
There is a deep peace in this nomadism. It teaches you that stability does not come from standing still. Stability comes from *internal coherence*.
|
||||
|
||||
By holding your own private key, you hold your own center of gravity. The world around you; the infrastructure, the topography and the availability of links can all shift chaotically. Storms can knock out towers. Cables can be cut. The internet can go down.
|
||||
|
||||
But as long as you possess your key, you possess your identity. The entire infrastructure can be destroyed and rebuilt, and you are still you. Nothing lasts, yet nothing is lost.
|
||||
|
||||
You become a sovereign entity moving through the noise, connected not by the rigidity of cables, but by the fluidity of recognition. The network becomes a place you inhabit, rather than a utility you subscribe to: You are at home in the ether.
|
||||
|
||||
|
||||
## VI: Ethics Of The Tool
|
||||
**Technology With Conscience**
|
||||
|
||||
You have unlearned the center. You have accepted the physics of trust. You have embraced the economy of scarcity and the freedom of unbound nomadism. You are standing in a new space. Now, look at the tool in your hand.
|
||||
|
||||
In the old world, we were taught that technology is neutral. We are told that "guns don't kill people, people do", or that a component is just a component, indifferent to what its combinatorial potential is. This is a convenient lie. It serves only to allow the builders to wash their hands of responsibility.
|
||||
|
||||
But we know better now. We know that **architecture is politics**, and *politics is control*. The way you build a system determines how it will be used. If you build a system optimized for mass surveillance, you *will* get a panopticon. If you build a system optimized for centralized control, you *will* get a dictatorship. If you build a system optimized for extraction, you *will* get a parasite.
|
||||
|
||||
The Zen of Reticulum asserts that a tool is never neutral.
|
||||
|
||||
On the very contrary: A tool is intent, **crystallized**.
|
||||
|
||||
### The Harm Principle
|
||||
|
||||
Why does the Reticulum License forbid the software from being used in systems designed to harm humans? Is it not just a restriction on freedom?
|
||||
|
||||
It is a restriction on *license*, yes, but it is an expansion of *freedom*.
|
||||
|
||||
Building powerful tools without a moral compass is in no way virtuous or commendable, it is plain and simple irresponsibility.
|
||||
|
||||
A tool that can easily be used to oppress is a real danger to the user. If you build a network that can be turned against you by a tyrant, you are not free. You are merely waiting for the leash to tighten. By encoding the "Harm Principle" into the legal DNA of the reference implementation, we are building a safeguard. We are stating, clearly and immutably, that *this tool* is for **life**, not for death.
|
||||
|
||||
This aligns the software with the interests of humanity. It cements that the network cannot be conscripted into a kill-system, a weaponized drone controller, or a torture device without breaking the license and the law. It is a line drawn in the sand - not by a government or external authority, but by the creators of the tool itself.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** *"It's just software. How people use it is not my problem."*
|
||||
- **The Zen Way:** *"This software is a habitat. I will not allow it to be used to build a cage."*
|
||||
|
||||
It is *your* choice whether to align with this - we are not forcing this stance on anyone. If you choose to align with life over death, with creativity over destruction, we grant you an immensely powerful tool, to own and build with as you please. If you do not, we deny it.
|
||||
|
||||
If you do not like this, we most assuredly do not need you here, and you are on your own.
|
||||
|
||||
### Public Domain Protocol
|
||||
|
||||
This leads to a vital distinction: The difference between the *idea* and the *implementation*.
|
||||
|
||||
The protocol - the mathematical rules of how Reticulum works - is dedicated to the Public Domain. It belongs to humanity. **No one can own it**. Anyone can implement it, improve it, or adapt it. This is the core idea of free communication, which itself must be forever free.
|
||||
|
||||
But the functional, deployed *reference implementation* - the Python code, the maintenance, the years of labor - has a conscience. This distinction is the engine of sustainability. It allows the protocol to be universal, while ensuring that the specific labor of the builders is not hijacked to undermine the foundational intent of the project itself. From this document, it should be very clear what this intent is.
|
||||
|
||||
If you want to build a system with Reticulum that manipulates and damages users for profits or targets missiles, you can use the public domain protocol, and start from scratch. But you cannot take our work. You must do your own. This serves as a pillar of accountability. If you want to build a weapon, *you* go and forge the steel yourself, while the world observes. And when the blood is drawn - it is on **your** hands.
|
||||
|
||||
### Preserving Human Agency
|
||||
|
||||
We live in an era of predatory extraction. The open-source commons is being scraped, ingested, and regurgitated by machine learning algorithms, whose corporate owners seek to replace the very humans who built those commons. Our code, our words, and our creativity is being used to train systems that are specifically designed to make us obsolete, without offering anything else in return than serfdom and leashes.
|
||||
|
||||
Reticulum stands against this.
|
||||
|
||||
The license protects the software from being used to feed the beast. It draws a hard line: This tool is for *people*. It is for human-to-human connection. It is not a dataset to be strip-mined for the purpose of building a synthetic overlord, puppeteered by a miniscule conglomerate of controllers.
|
||||
|
||||
This is a radical act of preservation. By protecting the code from AI appropriation, we are protecting space for human agency. We are ensuring that there remains a digital realm where the actors are flesh, blood and soul, where decisions are made by minds, not overlords hiding behind models.
|
||||
|
||||
When you use Reticulum, you are using a tool that respects you. It does not see you as a product to be tracked. It does not see your data as fuel for an algorithm. It sees you as a sovereign, equal peer.
|
||||
|
||||
This changes the foundational premise of using the technology. It restores dignity to the interaction. You are not the user of a service; you are a participant in a mutual covenant. The tool aligns with your autonomy, rather than eroding it.
|
||||
|
||||
In this way, ethics is not a restriction, but a foundation. It is the foundation that helps ensure the network will still belong to you tomorrow.
|
||||
|
||||
|
||||
## VII: Design Patterns For Post-IP Systems
|
||||
**Practical Philosophy for Developers**
|
||||
|
||||
The philosophy is useless if it cannot be hammered into code. The metaphors we have explored - nomadism, scarcity, trust - are not just poetry, but real-world engineering constraints. When you sit down to write software for Reticulum, these concepts must shape the very structure of your application.
|
||||
|
||||
We are now moving from the *why* to the *how*. This is where the abstract becomes concrete, and where you will see the true depth of the patterns we have been weaving.
|
||||
|
||||
### Store & Forward
|
||||
|
||||
The web has trained us to be impatient. We write synchronous code. We fire a request and we wait, blocking the UI, holding our breath. If the response doesn't come in 250 milliseconds, we show a spinner. If it doesn't come in five seconds, we show an error. We treat network connectivity as a binary state: either we are "online" or we are "broken".
|
||||
|
||||
This is brittle. It is a rejection of reality.
|
||||
|
||||
In Reticulum, connectivity is a spectrum, and presence is asynchronous. If at all applicable to your intent, you must design your applications to embrace **Store & Forward**.
|
||||
|
||||
Instead of demanding an immediate answer, your application should act as a patient participant. You create a message for someone or something in the mesh. The network holds it. It carries it from node to node, perhaps over hours or days, waiting for the recipient to appear. When they finally surface, the message is delivered. This requires a shift from "request/response" to "event/handler". How exactly you do this is a challenge for you to solve intelligently within your problem domain, but Reticulum-based systems already exist that does this extremely well, and you can use them for inspiration.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** `Connect() -> Send() -> Wait() -> Crash if timeout.`
|
||||
- **The Zen Way:** `Send() -> Continue living. -> Receive() when it arrives.`
|
||||
|
||||
This changes the user experience profoundly. It removes the anxiety of the loading bar. It creates a sense of continuity. The user is not "waiting for the network"; they are interacting with a persistent log of communication that lives in the network itself.
|
||||
|
||||
### Naming Is Power
|
||||
|
||||
In the IP world, we are slaves to the Domain Name System. We rely on a hierarchy of registrars to map human-readable names to machine-readable addresses. This hierarchy is a choke point. If the registrar revokes your domain, or if the DNS server goes down, you vanish.
|
||||
|
||||
Reticulum dissolves this hierarchy with **Hash-based Identity**.
|
||||
|
||||
In this design pattern, a name is not a string you look up; it is a cryptographic destination you verify. When you design for Reticulum, you stop asking the user for a URL and start asking for a Destination or Identity Hash.
|
||||
|
||||
This feels strange at first. A hash like `<83b7328926fed0d2e6a10a7671f9e237>` looks alien compared to `myfriend.com`. But that alienness is the armor. It **cannot** be spoofed. It **cannot** be censored by a registrar. It is **absolute**.
|
||||
|
||||
Designing for this means shifting your UI metaphors. You are no longer browsing a web of pages; you are managing a ledger of keys. You are building an "Address Book" that is actually a keyring. The names are given by the user, and the power stays with them. That hashes look complex is directly analogous to the strengths of the bonds formed by their use. It forces the user to engage in a moment of verification, an out-of-band handshake, which restores the human element of trust that SSL certificates stripped away.
|
||||
|
||||
### The Interface Is The Medium
|
||||
|
||||
One of the most liberating patterns in Reticulum is **Transport Agnosticism**.
|
||||
|
||||
In traditional networking, your code is often littered with transport logic. "Am I on WiFi? Check bandwidth. Am I on Cellular? Check data plan. Am I on Ethernet?". You are constantly micromanaging the pipe.
|
||||
|
||||
In Reticulum, you write to the API, and the API writes to the medium. You send a packet to a Destination. You do not care if that packet travels over a TCP tunnel, a LoRa radio wave, or a serial wire interface. That is the stack's concern.
|
||||
|
||||
This allows you to write **Universal Applications**.
|
||||
Imagine a messaging app. You write it once. It works on a laptop connected to fiber. It works on a phone in the city using WiFi. And, without a single line of code changed, it works on a device in the wilderness, talking only to other devices via radio.
|
||||
|
||||
The pattern is simple: **Never code to the hardware. Code to the intent.**
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** `socket.connect(ip, port)`
|
||||
- **The Zen Way:** `RNS.Packet(destination, data).send()`
|
||||
|
||||
By abstracting the medium, you make your software immortal to changes in infrastructure. The user might switch from a 4G hotspot to a HF modem tomorrow. Your software doesn't need to know. It simply continues the conversation.
|
||||
|
||||
### Emergent Patterns
|
||||
|
||||
When you combine these patterns - *Store & Forward*, *Hash-based Identity*, and *Transport Agnosticism* - you create software that feels fundamentally different.
|
||||
|
||||
It feels *grounded*. It doesn't flicker when the signal drops. It doesn't panic when the server is down. It has weight. It has persistence. It has *relevance*.
|
||||
|
||||
You are no longer building a "client" that begs a "server" for attention. You are building an autonomous agent that exists within the mesh. It speaks when it needs to, listens when it can, and carries its identity with it wherever it goes.
|
||||
|
||||
This is the culmination of the Zen. The code is not just a set of instructions: It is a behavioral envelope. It is a way of *being* in the network.
|
||||
|
||||
|
||||
## VIII: Fabric Of The Independent
|
||||
|
||||
We have stripped away the illusions. We have seen that the center is empty, that trust *must* be hard, that resources are finite, and that we must own our infrastructure. We have seen that tools have ethics and that our identity can move fluidly.
|
||||
|
||||
This is a reclaiming of the commons. For too long, we have allowed the most vital substrate of human society - *our ability to speak to one another* - to be colonized by entities that do not share our interests. We have allowed the architecture of our communication to be designed by accountants rather than architects.
|
||||
|
||||
We are taking it back. Not by petitioning the masters, but by building the new world within, over, under and around the shell of the old.
|
||||
|
||||
### The Work Is Finished
|
||||
|
||||
The heavy lifting is done.
|
||||
|
||||
The protocol is in the public domain, a gift to humanity that can never be taken away. The software is written, tested, and running on devices scattered across the globe. The manual lies open before you. The source code for the reference implementation is now distributed on hundreds of thousands of devices across the planet. No one can delete or destroy it. The hardware is accessible and abundant.
|
||||
|
||||
It was a hard road to get here, but we got here. Now, there is no roadmap committee waiting for approval. There is no venture capital dictating the user experience. There is no CEO to sign off on the next feature release.
|
||||
|
||||
There is only you.
|
||||
|
||||
The barrier to entry is no longer complexity: It is the mere habit of dependency. You were conditioned to wait. Wait for the app update. Wait for the ISP to fix the line. Wait for the platform to allow the post. Wait for the government to change the policies. Wait for the likes. Wait for the revolution to be televised.
|
||||
|
||||
The revolution never was televised.
|
||||
|
||||
It is packetized.
|
||||
|
||||
### Open Sky
|
||||
|
||||
The future of this technology is a construction project.
|
||||
|
||||
It looks like a single node on a windowsill, listening to the static. It looks like a message sent to a neighbor, bypassing the noise of the commercial web. It looks like a community mesh that grows, link by link, hop by hop, carried by hands that care more about connection than profit.
|
||||
|
||||
You have the blueprints. You have the tools. You have the philosophy. The noise of the old world has fallen away, leaving you with the quiet clarity of the open spectrum.
|
||||
|
||||
*Mark, early 2026*
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
# Windows Location Provider plugin example, provided by @haplo-dev
|
||||
|
||||
import RNS
|
||||
import time
|
||||
import threading
|
||||
import asyncio
|
||||
from winsdk.windows.devices import geolocation
|
||||
|
||||
class WindowsLocationPlugin(SidebandTelemetryPlugin):
|
||||
plugin_name = "windows_location"
|
||||
|
||||
def __init__(self, sideband_core):
|
||||
self.update_interval = 5.0
|
||||
self.should_run = False
|
||||
|
||||
self.latitude = None
|
||||
self.longitude = None
|
||||
self.altitude = None
|
||||
self.speed = None
|
||||
self.bearing = None
|
||||
self.accuracy = None
|
||||
self.last_update = None
|
||||
|
||||
super().__init__(sideband_core)
|
||||
|
||||
def start(self):
|
||||
RNS.log("Starting Windows Location provider plugin...")
|
||||
|
||||
self.should_run = True
|
||||
update_thread = threading.Thread(target=self.update_job, daemon=True)
|
||||
update_thread.start()
|
||||
|
||||
super().start()
|
||||
|
||||
def stop(self):
|
||||
self.should_run = False
|
||||
super().stop()
|
||||
|
||||
def update_job(self):
|
||||
while self.should_run:
|
||||
RNS.log("Updating location from Windows Geolocation...", RNS.LOG_DEBUG)
|
||||
try:
|
||||
asyncio.run(self.get_location())
|
||||
except Exception as e:
|
||||
RNS.log(f"Error getting location: {str(e)}", RNS.LOG_ERROR)
|
||||
|
||||
time.sleep(self.update_interval)
|
||||
|
||||
async def get_location(self):
|
||||
geolocator = geolocation.Geolocator()
|
||||
position = await geolocator.get_geoposition_async()
|
||||
|
||||
self.last_update = time.time()
|
||||
self.latitude = position.coordinate.latitude
|
||||
self.longitude = position.coordinate.longitude
|
||||
self.altitude = position.coordinate.altitude
|
||||
self.accuracy = position.coordinate.accuracy
|
||||
|
||||
# Note: Windows Geolocation doesn't provide speed and bearing directly
|
||||
# You might need to calculate these from successive position updates
|
||||
self.speed = None
|
||||
self.bearing = None
|
||||
|
||||
def has_location(self):
|
||||
return all([self.latitude, self.longitude, self.altitude, self.accuracy]) is not None
|
||||
|
||||
def update_telemetry(self, telemeter):
|
||||
if self.is_running() and telemeter is not None:
|
||||
if self.has_location():
|
||||
RNS.log("Updating location from Windows Geolocation", RNS.LOG_DEBUG)
|
||||
if "location" not in telemeter.sensors:
|
||||
telemeter.synthesize("location")
|
||||
|
||||
telemeter.sensors["location"].latitude = self.latitude
|
||||
telemeter.sensors["location"].longitude = self.longitude
|
||||
telemeter.sensors["location"].altitude = self.altitude
|
||||
telemeter.sensors["location"].speed = self.speed
|
||||
telemeter.sensors["location"].bearing = self.bearing
|
||||
telemeter.sensors["location"].accuracy = self.accuracy
|
||||
telemeter.sensors["location"].stale_time = 5
|
||||
telemeter.sensors["location"].set_update_time(self.last_update)
|
||||
|
||||
else:
|
||||
RNS.log("No location from Windows Geolocation yet", RNS.LOG_DEBUG)
|
||||
|
||||
# Finally, tell Sideband what class in this
|
||||
# file is the actual plugin class.
|
||||
plugin_class = WindowsLocationPlugin
|
||||
16
libs/able/.gitignore
vendored
16
libs/able/.gitignore
vendored
|
|
@ -1,16 +0,0 @@
|
|||
.buildozer/
|
||||
bin/
|
||||
docs/_build/
|
||||
*~
|
||||
*.swp
|
||||
*.sublime-workspace
|
||||
*.pyo
|
||||
*.pyc
|
||||
*.so
|
||||
|
||||
build/
|
||||
dist/
|
||||
sdist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
1.0.16
|
||||
------
|
||||
|
||||
* Added `autoconnect` parameter to connection methods
|
||||
`#45 <https://github.com/b3b/able/issues/45>`_
|
||||
|
||||
1.0.15
|
||||
------
|
||||
|
||||
* Changing the wheel name to avoid installing a package from cache
|
||||
`#40 <https://github.com/b3b/able/issues/40>`_
|
||||
|
||||
1.0.14
|
||||
------
|
||||
|
||||
* Added event handler for bluetooth adapter state change
|
||||
`#39 <https://github.com/b3b/able/pull/39>`_ by `robgar2001 <https://github.com/robgar2001>`_
|
||||
* Removal of deprecated `convert_path` from setup script
|
||||
|
||||
1.0.13
|
||||
------
|
||||
|
||||
* Fixed build failure when pip isolated environment is used `#38 <https://github.com/b3b/able/issues/38>`_
|
||||
|
||||
1.0.12
|
||||
------
|
||||
|
||||
* Fixed crash on API level 31 (Android 12) `#37 <https://github.com/b3b/able/issues/37>`_
|
||||
* Added new optional `BluetoothDispatcher` parameter to specifiy required permissions: `runtime_permissions`.
|
||||
Runtime permissions that are required by by default:
|
||||
ACCESS_FINE_LOCATION, BLUETOOTH_SCAN, BLUETOOTH_CONNECT, BLUETOOTH_ADVERTISE
|
||||
* Changed `able.require_bluetooth_enabled` behavior: first asks for runtime permissions
|
||||
and if permissions are granted then offers to enable the adapter
|
||||
* `require_runtime_permissions` decorator deprecated
|
||||
|
||||
1.0.11
|
||||
------
|
||||
|
||||
* Improved logging of reconnection management
|
||||
`#33 <https://github.com/b3b/able/pull/33>`_ by `robgar2001 <https://github.com/robgar2001>`_
|
||||
|
||||
1.0.10
|
||||
------
|
||||
|
||||
* Fixed build failure after AAB support was added to python-for-android
|
||||
|
||||
1.0.9
|
||||
-----
|
||||
|
||||
* Switched from deprecated scanning method `BluetoothAdapter.startLeScan` to `BluetoothLeScanner.startScan`
|
||||
* Added support for BLE scanning settings: `able.scan_settings` module
|
||||
* Added support for BLE scanning filters: `able.filters` module
|
||||
|
||||
|
||||
1.0.8
|
||||
-----
|
||||
|
||||
* Added support to use `able` in Android services
|
||||
* Added decorators:
|
||||
|
||||
- `able.require_bluetooth_enabled`: to call `BluetoothDispatcher` method when bluetooth adapter becomes ready
|
||||
- `able.require_runtime_permissions`: to call `BluetoothDispatcher` method when location runtime permission is granted
|
||||
|
||||
|
||||
1.0.7
|
||||
-----
|
||||
|
||||
* Added `able.advertising`: module to perform BLE advertise operations
|
||||
* Added property to get and set Bluetoth adapter name
|
||||
|
||||
|
||||
1.0.6
|
||||
-----
|
||||
|
||||
* Fixed `TypeError` exception on `BluetoothDispatcher.enable_notifications`
|
||||
|
||||
|
||||
1.0.5
|
||||
-----
|
||||
|
||||
* Added `BluetoothDispatcher.bonded_devices` property: list of paired BLE devices
|
||||
|
||||
1.0.4
|
||||
-----
|
||||
|
||||
* Fixed sending string data with `write_characteristic` function
|
||||
|
||||
1.0.3
|
||||
-----
|
||||
|
||||
* Changed package version generation:
|
||||
|
||||
- Version is set during the build, from the git tag
|
||||
- Development (git master) version is always "0.0.0"
|
||||
* Added ATT MTU changing method and callback
|
||||
* Added MTU changing example
|
||||
* Fixed:
|
||||
|
||||
- set `BluetoothDispatcher.gatt` attribute in GATT connection handler,
|
||||
to avoid possible `on_connection_state_change()` call before the `gatt` attribute is set
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 b3b
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
include LICENSE
|
||||
include README.rst
|
||||
include CHANGELOG.rst
|
||||
include able/src/org/able/*.java
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
from enum import IntEnum
|
||||
|
||||
from able.structures import Advertisement, Services
|
||||
from able.version import __version__ # noqa
|
||||
from kivy.utils import platform
|
||||
|
||||
__all__ = (
|
||||
"Advertisement",
|
||||
"BluetoothDispatcher",
|
||||
"Services",
|
||||
)
|
||||
|
||||
# constants
|
||||
GATT_SUCCESS = 0 #: GATT operation completed successfully
|
||||
STATE_CONNECTED = 2 #: The profile is in connected state
|
||||
STATE_DISCONNECTED = 0 #: The profile is in disconnected state
|
||||
|
||||
|
||||
class AdapterState(IntEnum):
|
||||
"""Bluetooth adapter state constants.
|
||||
https://developer.android.com/reference/android/bluetooth/BluetoothAdapter#STATE_OFF
|
||||
"""
|
||||
|
||||
OFF = 10 #: Adapter is off
|
||||
TURNING_ON = 11 #: Adapter is turning on
|
||||
ON = 12 #: Adapter is on
|
||||
TURNING_OFF = 13 #: Adapter is turning off
|
||||
|
||||
|
||||
class WriteType(IntEnum):
|
||||
"""GATT characteristic write types constants."""
|
||||
|
||||
DEFAULT = (
|
||||
2 #: Write characteristic, requesting acknowledgement by the remote device
|
||||
)
|
||||
NO_RESPONSE = (
|
||||
1 #: Write characteristic without requiring a response by the remote device
|
||||
)
|
||||
SIGNED = 4 #: Write characteristic including authentication signature
|
||||
|
||||
|
||||
if platform == "android":
|
||||
from able.android.dispatcher import BluetoothDispatcher
|
||||
else:
|
||||
|
||||
# mock android and PyJNIus modules usage
|
||||
import sys
|
||||
from unittest.mock import Mock
|
||||
|
||||
sys.modules["android"] = Mock()
|
||||
sys.modules["android.permissions"] = Mock()
|
||||
jnius = Mock()
|
||||
|
||||
class mocked_autoclass(Mock):
|
||||
def __call__(self, *args, **kwargs):
|
||||
mock = Mock()
|
||||
mock.__repr__ = lambda s: f"jnius.autoclass('{args[0]}')"
|
||||
mock.SDK_INT = 255
|
||||
return mock
|
||||
|
||||
jnius.autoclass = mocked_autoclass()
|
||||
sys.modules["jnius"] = jnius
|
||||
|
||||
from able.dispatcher import BluetoothDispatcherBase
|
||||
|
||||
class BluetoothDispatcher(BluetoothDispatcherBase):
|
||||
"""Bluetooth Low Energy interface
|
||||
|
||||
:param queue_timeout: BLE operations queue timeout
|
||||
:param enable_ble_code: request code to identify activity that alows
|
||||
user to turn on Bluetooth adapter
|
||||
:param runtime_permissions: overridden list of
|
||||
:py:mod:`permissions <able.permissions>`
|
||||
to be requested on runtime.
|
||||
"""
|
||||
|
||||
|
||||
from able.adapter import require_bluetooth_enabled
|
||||
from able.permissions import Permission
|
||||
|
||||
|
||||
def require_runtime_permissions(method):
|
||||
"""Deprecated decorator, left for backwards compatibility."""
|
||||
return method
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
from dataclasses import dataclass, field
|
||||
from functools import partial, wraps
|
||||
from typing import Optional
|
||||
|
||||
from android import activity
|
||||
from android.permissions import (
|
||||
check_permission,
|
||||
request_permissions,
|
||||
)
|
||||
from jnius import autoclass
|
||||
from kivy.logger import Logger
|
||||
|
||||
|
||||
Activity = autoclass("android.app.Activity")
|
||||
|
||||
|
||||
def require_bluetooth_enabled(method):
|
||||
"""Decorator to call `BluetoothDispatcher` method
|
||||
when runtime permissions are granted
|
||||
and Bluetooth adapter becomes ready.
|
||||
|
||||
Decorator launches system activities that allows the user
|
||||
to grant runtime permissions and turn on Bluetooth,
|
||||
if Bluetooth is not enabled.
|
||||
"""
|
||||
|
||||
@wraps(method)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
manager = AdapterManager.get_attached_manager(self)
|
||||
if manager:
|
||||
return manager.execute(partial(method, self, *args, **kwargs))
|
||||
return None
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def set_adapter_failure_rollback(handler):
|
||||
"""Decorator to launch handler
|
||||
if permissions are not granted or adapter is not enabled."""
|
||||
|
||||
def inner(func):
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
manager = AdapterManager.get_attached_manager(self)
|
||||
if manager:
|
||||
manager.rollback_handlers.append(partial(handler, self))
|
||||
return func(self, *args, **kwargs)
|
||||
return None
|
||||
|
||||
return wrapper
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdapterManager:
|
||||
"""
|
||||
Class for managing the execution of operations
|
||||
that require the BLE adapter.
|
||||
Operations are deferred until runtime permissions are granted
|
||||
and the BLE adapter is enabled.
|
||||
"""
|
||||
|
||||
ble: "org.able.BLE"
|
||||
enable_ble_code: str
|
||||
runtime_permissions: list
|
||||
operations: list = field(default_factory=list)
|
||||
rollback_handlers: list = field(default_factory=list)
|
||||
is_permissions_granted: bool = False
|
||||
is_permissions_requested: bool = False
|
||||
is_adapter_requested: bool = False
|
||||
|
||||
@property
|
||||
def adapter(self) -> Optional["android.bluetooth.BluetoothAdapter"]:
|
||||
if self.has_permissions:
|
||||
adapter = self.ble.mBluetoothAdapter
|
||||
if adapter and adapter.isEnabled():
|
||||
return adapter
|
||||
return None
|
||||
|
||||
@property
|
||||
def has_permissions(self):
|
||||
if not self.is_permissions_granted:
|
||||
self.is_permissions_granted = self.check_permissions()
|
||||
return self.is_permissions_granted
|
||||
|
||||
@property
|
||||
def is_service_context(self):
|
||||
return not activity._activity
|
||||
|
||||
def __post_init__(self):
|
||||
if self.is_service_context:
|
||||
self.is_permissions_granted = True
|
||||
else:
|
||||
activity.bind(on_activity_result=self.on_activity_result)
|
||||
|
||||
@classmethod
|
||||
def get_attached_manager(cls, instance):
|
||||
manager = getattr(instance, "_adapter_manager", None)
|
||||
if not manager:
|
||||
Logger.error("BLE adapter manager is not installed")
|
||||
return manager
|
||||
|
||||
def install(self, instance):
|
||||
setattr(instance, "_adapter_manager", self)
|
||||
|
||||
def check_permissions(self):
|
||||
return all(
|
||||
[check_permission(permission) for permission in self.runtime_permissions]
|
||||
)
|
||||
|
||||
def request_permissions(self):
|
||||
if self.is_permissions_requested:
|
||||
return
|
||||
self.is_permissions_requested = True
|
||||
if not self.is_service_context:
|
||||
Logger.debug("Request runtime permissions")
|
||||
request_permissions(
|
||||
self.runtime_permissions,
|
||||
self.on_runtime_permissions,
|
||||
)
|
||||
else:
|
||||
Logger.error("Required permissions are not granted for service")
|
||||
|
||||
def request_adapter(self):
|
||||
if self.is_adapter_requested:
|
||||
return
|
||||
self.is_adapter_requested = True
|
||||
self.ble.getAdapter(self.enable_ble_code)
|
||||
|
||||
def rollback(self):
|
||||
self._execute_operations(self.rollback_handlers)
|
||||
|
||||
def execute(self, operation):
|
||||
if self.adapter:
|
||||
# execute immediately, if adapter is enabled
|
||||
return operation()
|
||||
self.operations.append(operation)
|
||||
self.execute_operations()
|
||||
|
||||
def execute_operations(self):
|
||||
if self.has_permissions:
|
||||
if self.adapter:
|
||||
self._execute_operations(self.operations)
|
||||
else:
|
||||
self.request_adapter()
|
||||
else:
|
||||
self.request_permissions()
|
||||
|
||||
def _execute_operations(self, operations):
|
||||
self.operations = []
|
||||
self.rollback_handlers = []
|
||||
for operation in operations:
|
||||
try:
|
||||
operation()
|
||||
except Exception as exc:
|
||||
Logger.exception(exc)
|
||||
|
||||
def on_runtime_permissions(self, permissions, grant_results):
|
||||
granted = all(grant_results)
|
||||
self.is_permissions_granted = granted
|
||||
self.is_permissions_requested = False # allow future invocations
|
||||
if granted:
|
||||
Logger.debug("Required permissions are granted")
|
||||
self.execute_operations()
|
||||
else:
|
||||
Logger.error("Required permissions are not granted")
|
||||
self.rollback()
|
||||
|
||||
def on_activity_result(self, requestCode, resultCode, intent):
|
||||
if requestCode == self.enable_ble_code:
|
||||
enabled = resultCode == Activity.RESULT_OK
|
||||
self.is_adapter_requested = False # allow future invocations
|
||||
if enabled:
|
||||
Logger.debug("BLE adapter is enabled")
|
||||
self.execute_operations()
|
||||
else:
|
||||
Logger.error("BLE adapter is not enabled")
|
||||
self.rollback()
|
||||
|
|
@ -1,330 +0,0 @@
|
|||
"""BLE advertise operations."""
|
||||
from abc import abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from jnius import JavaException, autoclass
|
||||
from kivy.event import EventDispatcher
|
||||
|
||||
from able.android.dispatcher import BluetoothDispatcher
|
||||
from able.android.jni import PythonBluetoothAdvertiser
|
||||
from able.utils import force_convertible_to_java_array
|
||||
|
||||
|
||||
AdvertiseDataBuilder = autoclass('android.bluetooth.le.AdvertiseData$Builder')
|
||||
AdvertisingSet = autoclass('android.bluetooth.le.AdvertisingSet')
|
||||
AdvertisingSetParametersBuilder = autoclass('android.bluetooth.le.AdvertisingSetParameters$Builder')
|
||||
AndroidAdvertiseData = autoclass('android.bluetooth.le.AdvertiseData')
|
||||
BluetoothLeAdvertiser = autoclass('android.bluetooth.le.BluetoothLeAdvertiser')
|
||||
ParcelUuid = autoclass('android.os.ParcelUuid')
|
||||
|
||||
BLEAdvertiser = autoclass('org.able.BLEAdvertiser')
|
||||
|
||||
|
||||
class Interval(IntEnum):
|
||||
"""Advertising interval constants.
|
||||
https://developer.android.com/reference/android/bluetooth/le/AdvertisingSetParameters#INTERVAL_HIGH
|
||||
"""
|
||||
MIN = 160 #: Minimum value for advertising interval, around every 100ms
|
||||
MEDIUM = 400 #: Advertise on medium frequency, around every 250ms
|
||||
HIGH = 1600 #: Advertise on low frequency, around every 1000ms
|
||||
MAX = 16777215 #: Maximum value for advertising interval
|
||||
|
||||
|
||||
class TXPower(IntEnum):
|
||||
"""Advertising transmission (TX) power level constants.
|
||||
https://developer.android.com/reference/android/bluetooth/le/AdvertisingSetParameters#TX_POWER_HIGH
|
||||
"""
|
||||
MIN = -127 #: Minimum value for TX power
|
||||
ULTRA_LOW = -21 #: Advertise using the lowest TX power level
|
||||
LOW = -15 #: Advertise using the low TX power level
|
||||
MEDIUM = -7 #: Advertise using the medium TX power level
|
||||
MAX = 1 #: Maximum value for TX power
|
||||
|
||||
|
||||
class Status:
|
||||
"""Advertising operation status constants.
|
||||
https://developer.android.com/reference/android/bluetooth/le/AdvertisingSetCallback#constants
|
||||
"""
|
||||
SUCCESS = 0
|
||||
DATA_TOO_LARGE = 1
|
||||
TOO_MANY_ADVERTISERS = 2
|
||||
ALREADY_STARTED = 3
|
||||
INTERNAL_ERROR = 4
|
||||
FEATURE_UNSUPPORTED = 5
|
||||
|
||||
|
||||
@dataclass
|
||||
class ADStructure:
|
||||
|
||||
@abstractmethod
|
||||
def add_payload(self, builder: AdvertiseDataBuilder):
|
||||
pass
|
||||
|
||||
|
||||
class DeviceName(ADStructure):
|
||||
"""Include device name (complete local name) in advertise packet."""
|
||||
|
||||
def add_payload(self, builder):
|
||||
builder.setIncludeDeviceName(True)
|
||||
|
||||
|
||||
class TXPowerLevel(ADStructure):
|
||||
"""Include transmission power level in the advertise packet."""
|
||||
|
||||
def add_payload(self, builder):
|
||||
builder.setIncludeTxPowerLevel(True)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceUUID(ADStructure):
|
||||
"""Service UUID to advertise.
|
||||
|
||||
:param uid: UUID to be advertised
|
||||
"""
|
||||
uid: str
|
||||
|
||||
def add_payload(self, builder):
|
||||
builder.addServiceUuid(
|
||||
ParcelUuid.fromString(self.uid)
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceData(ADStructure):
|
||||
"""Service data to advertise.
|
||||
|
||||
:param uid: UUID of the service the data is associated with
|
||||
:param data: Service data
|
||||
"""
|
||||
|
||||
uid: str
|
||||
data: Union[list, tuple, bytes, bytearray]
|
||||
|
||||
def add_payload(self, builder):
|
||||
builder.addServiceData(
|
||||
ParcelUuid.fromString(self.uid),
|
||||
force_convertible_to_java_array(self.data)
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ManufacturerData(ADStructure):
|
||||
"""Manufacturer specific data to advertise.
|
||||
|
||||
:param id: Manufacturer ID
|
||||
:param data: Manufacturer specific data
|
||||
"""
|
||||
id: int
|
||||
data: Union[list, tuple, bytes, bytearray]
|
||||
|
||||
def add_payload(self, builder):
|
||||
builder.addManufacturerData(
|
||||
self.id,
|
||||
force_convertible_to_java_array(self.data)
|
||||
)
|
||||
|
||||
|
||||
class AdvertiseData:
|
||||
"""Builder for data payload to be advertised.
|
||||
|
||||
:param payload: List of AD structures to include in advertisement
|
||||
|
||||
>>> AdvertiseData(DeviceName(), ManufacturerData(10, b'specific data'))
|
||||
[DeviceName(), ManufacturerData(id=10, data=b'specific data')]
|
||||
"""
|
||||
|
||||
def __init__(self, *payload: List[ADStructure]):
|
||||
self.payload = payload
|
||||
self.data = self.build()
|
||||
|
||||
def __repr__(self):
|
||||
sections = ", ".join(repr(ad) for ad in self.payload)
|
||||
return f"[{sections}]"
|
||||
|
||||
def build(self) -> AndroidAdvertiseData:
|
||||
builder = AdvertiseDataBuilder()
|
||||
for ad in self.payload:
|
||||
ad.add_payload(builder)
|
||||
return builder.build()
|
||||
|
||||
|
||||
class Advertiser(EventDispatcher):
|
||||
"""Base class for BLE advertise operations.
|
||||
|
||||
:param ble: BLE interface instance
|
||||
:param data: Advertisement data to be broadcasted
|
||||
:param scan_data: Scan response associated with the advertisement data
|
||||
:param interval: Advertising interval
|
||||
`<https://developer.android.com/reference/android/bluetooth/le/AdvertisingSetParameters.Builder#setInterval(int)>`_
|
||||
:param tx_power: Transmission power level
|
||||
`<https://developer.android.com/reference/android/bluetooth/le/AdvertisingSetParameters.Builder#setTxPowerLevel(int)>`_
|
||||
|
||||
>>> Advertiser(
|
||||
... ble=BluetoothDispatcher(),
|
||||
... data=AdvertiseData(DeviceName()),
|
||||
... scan_data=AdvertiseData(TXPowerLevel()),
|
||||
... interval=Interval.MIN,
|
||||
... tx_power=TXPower.MAX
|
||||
... ) #doctest: +ELLIPSIS
|
||||
<able.advertising.Advertiser object at 0x...>
|
||||
"""
|
||||
|
||||
__events__ = (
|
||||
'on_advertising_started',
|
||||
'on_advertising_stopped',
|
||||
'on_advertising_enabled',
|
||||
'on_advertising_data_set',
|
||||
'on_scan_response_data_set',
|
||||
'on_advertising_parameters_updated',
|
||||
'on_advertising_set_changed',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ble: BluetoothDispatcher,
|
||||
data: AdvertiseData = None,
|
||||
scan_data: AdvertiseData = None,
|
||||
interval: int = Interval.HIGH,
|
||||
tx_power: int = TXPower.MEDIUM,
|
||||
):
|
||||
self._ble = ble
|
||||
self._data = data
|
||||
self._scan_data = scan_data
|
||||
self._interval = interval
|
||||
self._tx_power = tx_power
|
||||
|
||||
self._events_interface = PythonBluetoothAdvertiser(self)
|
||||
self._advertiser = BLEAdvertiser(self._events_interface)
|
||||
self._callback_set = self._advertiser.mCallbackSet
|
||||
self._advertising_set = None
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
"""
|
||||
:setter: Update advertising data
|
||||
:type: Optional[AdvertiseData]
|
||||
"""
|
||||
return self._data
|
||||
|
||||
@data.setter
|
||||
def data(self, value):
|
||||
self._data = value
|
||||
self._update_advertising_set()
|
||||
|
||||
@property
|
||||
def scan_data(self):
|
||||
"""
|
||||
:setter: Update the scan response
|
||||
:type: Optional[AdvertiseData]
|
||||
"""
|
||||
return self._scan_data
|
||||
|
||||
@scan_data.setter
|
||||
def scan_data(self, value):
|
||||
self._scan_data = value
|
||||
self._update_advertising_set()
|
||||
|
||||
@property
|
||||
def interval(self):
|
||||
"""
|
||||
:setter: Update the advertising interval
|
||||
:type: int
|
||||
"""
|
||||
return self._interval
|
||||
|
||||
@interval.setter
|
||||
def interval(self, value):
|
||||
self._interval = value
|
||||
self._update_advertising_set()
|
||||
|
||||
@property
|
||||
def tx_power(self):
|
||||
"""
|
||||
:setter: Update the transmission power level
|
||||
:type: int
|
||||
"""
|
||||
return self._tx_power
|
||||
|
||||
@tx_power.setter
|
||||
def tx_power(self, value):
|
||||
self._tx_power = value
|
||||
self._update_advertising_set()
|
||||
|
||||
@property
|
||||
def bluetooth_le_advertiser(self) -> Optional[BluetoothLeAdvertiser]:
|
||||
adapter = self._ble.adapter
|
||||
return adapter and adapter.getBluetoothLeAdvertiser()
|
||||
|
||||
@property
|
||||
def parameters(self) -> AdvertisingSetParametersBuilder:
|
||||
builder = AdvertisingSetParametersBuilder()
|
||||
builder.setLegacyMode(True) \
|
||||
.setConnectable(False) \
|
||||
.setScannable(True) \
|
||||
.setInterval(self._interval) \
|
||||
.setTxPowerLevel(self._tx_power)
|
||||
return builder.build()
|
||||
|
||||
def start(self):
|
||||
"""Start advertising.
|
||||
|
||||
Start a system activity that allows the user to turn on Bluetooth if Bluetooth is not enabled.
|
||||
"""
|
||||
if not self._advertising_set:
|
||||
self._ble._start_advertising(self)
|
||||
|
||||
def stop(self):
|
||||
"""Stop advertising."""
|
||||
advertiser = self.bluetooth_le_advertiser
|
||||
if advertiser:
|
||||
advertiser.stopAdvertisingSet(self._callback_set)
|
||||
|
||||
def on_advertising_started(self, advertising_set: AdvertisingSet, tx_power: int, status: Status):
|
||||
"""Handler for advertising start operation (onAdvertisingSetStarted).
|
||||
"""
|
||||
|
||||
def on_advertising_stopped(self, advertising_set: AdvertisingSet):
|
||||
"""Handler for advertising stop operation (onAdvertisingSetStopped)."""
|
||||
|
||||
def on_advertising_enabled(self, advertising_set: AdvertisingSet, enable: bool, status: Status):
|
||||
"""Handler for advertising enable/disable operation (onAdvertisingEnabled)."""
|
||||
|
||||
def on_advertising_data_set(self, advertising_set: AdvertisingSet, status: Status):
|
||||
"""Handler for data set operation (onAdvertisingDataSet)."""
|
||||
|
||||
def on_scan_response_data_set(self, advertising_set: AdvertisingSet, status: Status):
|
||||
"""Handler for scan response data set operation (onScanResponseDataSet)."""
|
||||
|
||||
def on_advertising_parameters_updated(self, advertising_set: AdvertisingSet, tx_power: int, status: Status):
|
||||
"""Handler for parameters set operation (onAdvertisingParametersUpdated)."""
|
||||
|
||||
def on_advertising_set_changed(self, advertising_set):
|
||||
self._advertising_set = advertising_set
|
||||
|
||||
def _start(self):
|
||||
advertiser = self.bluetooth_le_advertiser
|
||||
if advertiser:
|
||||
self._callback_set = self._advertiser.createCallback()
|
||||
try:
|
||||
advertiser.startAdvertisingSet(
|
||||
self.parameters,
|
||||
self._data and self._data.data,
|
||||
self._scan_data and self._scan_data.data,
|
||||
None, # periodicParameters
|
||||
None, # periodicData
|
||||
self._callback_set
|
||||
)
|
||||
except JavaException as exc:
|
||||
if exc.classname == 'java.lang.IllegalArgumentException' and \
|
||||
exc.innermessage.endswith('data too big'):
|
||||
self.dispatch('on_advertising_started', None, 0, Status.DATA_TOO_LARGE)
|
||||
raise
|
||||
|
||||
def _update_advertising_set(self):
|
||||
advertising_set = self._advertising_set
|
||||
if advertising_set:
|
||||
advertising_set.setAdvertisingParameters(self.parameters)
|
||||
advertising_set.setScanResponseData(self._scan_data and self._scan_data.data)
|
||||
advertising_set.setAdvertisingData(self._data and self._data.data)
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
from jnius import JavaException, autoclass
|
||||
from kivy.logger import Logger
|
||||
|
||||
from able.adapter import (
|
||||
AdapterManager,
|
||||
require_bluetooth_enabled,
|
||||
set_adapter_failure_rollback,
|
||||
)
|
||||
from able.android.jni import PythonBluetooth
|
||||
from able.dispatcher import BluetoothDispatcherBase
|
||||
from able.scan_settings import ScanSettingsBuilder
|
||||
|
||||
ArrayList = autoclass("java.util.ArrayList")
|
||||
|
||||
try:
|
||||
BLE = autoclass("org.able.BLE")
|
||||
except:
|
||||
Logger.error(
|
||||
"able_recipe: Failed to load Java class org.able.BLE. Possible build error."
|
||||
)
|
||||
raise
|
||||
else:
|
||||
Logger.info("able_recipe: org.able.BLE Java class loaded")
|
||||
|
||||
BluetoothAdapter = autoclass("android.bluetooth.BluetoothAdapter")
|
||||
BluetoothDevice = autoclass("android.bluetooth.BluetoothDevice")
|
||||
BluetoothGattDescriptor = autoclass("android.bluetooth.BluetoothGattDescriptor")
|
||||
|
||||
ENABLE_NOTIFICATION_VALUE = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
||||
ENABLE_INDICATION_VALUE = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
|
||||
DISABLE_NOTIFICATION_VALUE = BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
|
||||
|
||||
|
||||
class BluetoothDispatcher(BluetoothDispatcherBase):
|
||||
@property
|
||||
@require_bluetooth_enabled
|
||||
def adapter(self):
|
||||
return AdapterManager.get_attached_manager(self).adapter
|
||||
|
||||
@property
|
||||
def bonded_devices(self):
|
||||
ble_types = (BluetoothDevice.DEVICE_TYPE_LE, BluetoothDevice.DEVICE_TYPE_DUAL)
|
||||
adapter = self.adapter
|
||||
devices = adapter.getBondedDevices().toArray() if adapter else []
|
||||
return [dev for dev in devices if dev.getType() in ble_types]
|
||||
|
||||
def _set_ble_interface(self):
|
||||
self._events_interface = PythonBluetooth(self)
|
||||
self._ble = BLE(self._events_interface)
|
||||
|
||||
@set_adapter_failure_rollback(
|
||||
lambda self: self.dispatch("on_scan_started", success=False)
|
||||
)
|
||||
@require_bluetooth_enabled
|
||||
def start_scan(self, filters=None, settings=None):
|
||||
filters_array = ArrayList()
|
||||
for f in filters or []:
|
||||
filters_array.add(f.build())
|
||||
if not settings:
|
||||
settings = ScanSettingsBuilder()
|
||||
try:
|
||||
settings = settings.build()
|
||||
except AttributeError:
|
||||
pass
|
||||
self._ble.startScan(self.enable_ble_code, filters_array, settings)
|
||||
|
||||
def stop_scan(self):
|
||||
self._ble.stopScan()
|
||||
|
||||
@require_bluetooth_enabled
|
||||
def connect_by_device_address(self, address: str, autoconnect: bool = False):
|
||||
address = address.upper()
|
||||
if not BluetoothAdapter.checkBluetoothAddress(address):
|
||||
raise ValueError(f"{address} is not a valid Bluetooth address")
|
||||
adapter = self.adapter
|
||||
if adapter:
|
||||
self.connect_gatt(adapter.getRemoteDevice(address), autoconnect)
|
||||
|
||||
@require_bluetooth_enabled
|
||||
def enable_notifications(self, characteristic, enable=True, indication=False):
|
||||
if not self.gatt.setCharacteristicNotification(characteristic, enable):
|
||||
return False
|
||||
|
||||
if not enable:
|
||||
# DISABLE_NOTIFICAITON_VALUE is for disabling
|
||||
# both notifications and indications
|
||||
descriptor_value = DISABLE_NOTIFICATION_VALUE
|
||||
elif indication:
|
||||
descriptor_value = ENABLE_INDICATION_VALUE
|
||||
else:
|
||||
descriptor_value = ENABLE_NOTIFICATION_VALUE
|
||||
|
||||
for descriptor in characteristic.getDescriptors().toArray():
|
||||
self.write_descriptor(descriptor, descriptor_value)
|
||||
return True
|
||||
|
||||
@require_bluetooth_enabled
|
||||
def _start_advertising(self, advertiser):
|
||||
advertiser._start()
|
||||
|
||||
@require_bluetooth_enabled
|
||||
def _set_name(self, value):
|
||||
adapter = self.adapter
|
||||
if adapter:
|
||||
self.adapter.setName(value)
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
from able import GATT_SUCCESS
|
||||
from able.structures import Advertisement, Services
|
||||
from jnius import PythonJavaClass, java_method
|
||||
from kivy.logger import Logger
|
||||
|
||||
|
||||
class PythonBluetooth(PythonJavaClass):
|
||||
__javainterfaces__ = ['org.able.PythonBluetooth']
|
||||
__javacontext__ = 'app'
|
||||
|
||||
def __init__(self, dispatcher):
|
||||
super(PythonBluetooth, self).__init__()
|
||||
self.dispatcher = dispatcher
|
||||
|
||||
@java_method('(Ljava/lang/String;)V')
|
||||
def on_error(self, msg):
|
||||
Logger.debug("on_error")
|
||||
self.dispatcher.dispatch('on_error', msg)
|
||||
|
||||
@java_method('(Landroid/bluetooth/le/ScanResult;)V')
|
||||
def on_scan_result(self, result):
|
||||
device = result.getDevice() # type: android.bluetooth.BluetoothDevice
|
||||
record = result.getScanRecord() # type: android.bluetooth.le.ScanRecord
|
||||
if record:
|
||||
self.dispatcher.dispatch(
|
||||
'on_device',
|
||||
device,
|
||||
result.getRssi(),
|
||||
Advertisement(record.getBytes())
|
||||
)
|
||||
else:
|
||||
Logger.warning(
|
||||
"Scan result for device without the scan record: %s",
|
||||
device
|
||||
)
|
||||
|
||||
@java_method('(Z)V')
|
||||
def on_scan_started(self, success):
|
||||
Logger.debug("on_scan_started")
|
||||
self.dispatcher.dispatch('on_scan_started', success)
|
||||
|
||||
@java_method('()V')
|
||||
def on_scan_completed(self):
|
||||
Logger.debug("on_scan_completed")
|
||||
self.dispatcher.dispatch('on_scan_completed')
|
||||
|
||||
@java_method('(II)V')
|
||||
def on_connection_state_change(self, status, state):
|
||||
Logger.debug("on_connection_state_change status={} state: {}".format(
|
||||
status, state))
|
||||
self.dispatcher.dispatch('on_connection_state_change', status, state)
|
||||
|
||||
@java_method('(I)V')
|
||||
def on_bluetooth_adapter_state_change(self, state):
|
||||
Logger.debug("on_bluetooth_adapter_state_change state: {}".format(state))
|
||||
self.dispatcher.dispatch('on_bluetooth_adapter_state_change', state)
|
||||
|
||||
@java_method('(ILjava/util/List;)V')
|
||||
def on_services(self, status, services):
|
||||
services_dict = Services()
|
||||
if status == GATT_SUCCESS:
|
||||
for service in services.toArray():
|
||||
service_uuid = service.getUuid().toString()
|
||||
Logger.debug("Service discovered: {}".format(service_uuid))
|
||||
services_dict[service_uuid] = {}
|
||||
for c in service.getCharacteristics().toArray():
|
||||
characteristic_uuid = c.getUuid().toString()
|
||||
Logger.debug("Characteristic discovered: {}".format(
|
||||
characteristic_uuid))
|
||||
services_dict[service_uuid][characteristic_uuid] = c
|
||||
self.dispatcher.dispatch('on_services', status, services_dict)
|
||||
|
||||
@java_method('(Landroid/bluetooth/BluetoothGattCharacteristic;)V')
|
||||
def on_characteristic_changed(self, characteristic):
|
||||
# uuid = characteristic.getUuid().toString()
|
||||
self.dispatcher.dispatch('on_characteristic_changed', characteristic)
|
||||
|
||||
@java_method('(Landroid/bluetooth/BluetoothGattCharacteristic;I)V')
|
||||
def on_characteristic_read(self, characteristic, status):
|
||||
self.dispatcher.dispatch('on_gatt_release')
|
||||
# uuid = characteristic.getUuid().toString()
|
||||
self.dispatcher.dispatch('on_characteristic_read',
|
||||
characteristic,
|
||||
status)
|
||||
|
||||
@java_method('(Landroid/bluetooth/BluetoothGattCharacteristic;I)V')
|
||||
def on_characteristic_write(self, characteristic, status):
|
||||
self.dispatcher.dispatch('on_gatt_release')
|
||||
# uuid = characteristic.getUuid().toString()
|
||||
self.dispatcher.dispatch('on_characteristic_write',
|
||||
characteristic,
|
||||
status)
|
||||
|
||||
@java_method('(Landroid/bluetooth/BluetoothGattDescriptor;I)V')
|
||||
def on_descriptor_read(self, descriptor, status):
|
||||
self.dispatcher.dispatch('on_gatt_release')
|
||||
# characteristic = descriptor.getCharacteristic()
|
||||
# uuid = characteristic.getUuid().toString()
|
||||
self.dispatcher.dispatch('on_descriptor_read', descriptor, status)
|
||||
|
||||
@java_method('(Landroid/bluetooth/BluetoothGattDescriptor;I)V')
|
||||
def on_descriptor_write(self, descriptor, status):
|
||||
self.dispatcher.dispatch('on_gatt_release')
|
||||
# characteristic = descriptor.getCharacteristic()
|
||||
# uuid = characteristic.getUuid().toString()
|
||||
self.dispatcher.dispatch('on_descriptor_write', descriptor, status)
|
||||
|
||||
@java_method('(II)V')
|
||||
def on_rssi_updated(self, rssi, status):
|
||||
self.dispatcher.dispatch('on_gatt_release')
|
||||
self.dispatcher.dispatch('on_rssi_updated', rssi, status)
|
||||
|
||||
@java_method('(II)V')
|
||||
def on_mtu_changed(self, mtu, status):
|
||||
self.dispatcher.dispatch('on_gatt_release')
|
||||
self.dispatcher.dispatch('on_mtu_changed', mtu, status)
|
||||
|
||||
|
||||
class PythonBluetoothAdvertiser(PythonJavaClass):
|
||||
__javainterfaces__ = ['org.able.PythonBluetoothAdvertiser']
|
||||
__javacontext__ = 'app'
|
||||
|
||||
def __init__(self, dispatcher):
|
||||
super().__init__()
|
||||
self.dispatcher = dispatcher
|
||||
|
||||
@java_method('(Landroid/bluetooth/le/AdvertisingSet;II)V')
|
||||
def on_advertising_started(self, advertising_set, tx_power, status):
|
||||
self.dispatcher.dispatch('on_advertising_set_changed', advertising_set)
|
||||
self.dispatcher.dispatch('on_advertising_started', advertising_set, tx_power, status)
|
||||
|
||||
@java_method('(Landroid/bluetooth/le/AdvertisingSet;)V')
|
||||
def on_advertising_stopped(self, advertising_set):
|
||||
self.dispatcher.dispatch('on_advertising_set_changed', None)
|
||||
self.dispatcher.dispatch('on_advertising_stopped', advertising_set)
|
||||
|
||||
@java_method('(Landroid/bluetooth/le/AdvertisingSet;BI)V')
|
||||
def on_advertising_enabled(self, advertising_set, enable, status):
|
||||
self.dispatcher.dispatch('on_advertising_enabled', advertising_set, enable, status)
|
||||
|
||||
@java_method('(Landroid/bluetooth/le/AdvertisingSet;I)V')
|
||||
def on_advertising_data_set(self, advertising_set, status):
|
||||
self.dispatcher.dispatch('on_advertising_data_set', advertising_set, status)
|
||||
|
||||
@java_method('(Landroid/bluetooth/le/AdvertisingSet;I)V')
|
||||
def on_scan_response_data_set(self, advertising_set, status):
|
||||
self.dispatcher.dispatch('on_scan_response_data_set', advertising_set, status)
|
||||
|
||||
@java_method('(Landroid/bluetooth/le/AdvertisingSet;II)V')
|
||||
def on_advertising_parameters_updated(self, advertising_set, tx_power, status):
|
||||
self.dispatcher.dispatch('on_advertising_parameters_updated', advertising_set, tx_power, status)
|
||||
|
|
@ -1,371 +0,0 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from kivy.event import EventDispatcher
|
||||
from kivy.logger import Logger
|
||||
|
||||
from able import WriteType
|
||||
from able.adapter import AdapterManager
|
||||
from able.filters import Filter
|
||||
from able.permissions import DEFAULT_RUNTIME_PERMISSIONS
|
||||
from able.queue import BLEQueue, ble_task, ble_task_done
|
||||
from able.scan_settings import ScanSettingsBuilder
|
||||
from able.utils import force_convertible_to_java_array
|
||||
|
||||
|
||||
class BLEError:
|
||||
"""Raise Exception on attribute access"""
|
||||
|
||||
def __init__(self, msg):
|
||||
self.msg = msg
|
||||
|
||||
def __getattr__(self, name):
|
||||
raise Exception(self.msg)
|
||||
|
||||
|
||||
class BluetoothDispatcherBase(EventDispatcher):
|
||||
__events__ = (
|
||||
"on_device",
|
||||
"on_scan_started",
|
||||
"on_scan_completed",
|
||||
"on_services",
|
||||
"on_connection_state_change",
|
||||
"on_bluetooth_adapter_state_change",
|
||||
"on_characteristic_changed",
|
||||
"on_characteristic_read",
|
||||
"on_characteristic_write",
|
||||
"on_descriptor_read",
|
||||
"on_descriptor_write",
|
||||
"on_gatt_release",
|
||||
"on_error",
|
||||
"on_rssi_updated",
|
||||
"on_mtu_changed",
|
||||
)
|
||||
queue_class = BLEQueue
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
queue_timeout: float = 0.5,
|
||||
enable_ble_code: int = 0xAB1E,
|
||||
runtime_permissions: Optional[list[str]] = None, # DEFAULT_RUNTIME_PERMISSIONS
|
||||
):
|
||||
super(BluetoothDispatcherBase, self).__init__()
|
||||
self.queue_timeout = queue_timeout
|
||||
self.enable_ble_code = enable_ble_code
|
||||
self.runtime_permissions = [
|
||||
str(permission)
|
||||
for permission in (
|
||||
runtime_permissions
|
||||
if runtime_permissions is not None
|
||||
else DEFAULT_RUNTIME_PERMISSIONS
|
||||
)
|
||||
]
|
||||
self._remote_device_address = None
|
||||
self._set_ble_interface()
|
||||
self._set_queue()
|
||||
self._set_adapter_manager()
|
||||
|
||||
def _set_ble_interface(self):
|
||||
self._ble = BLEError("BLE is not implemented for platform")
|
||||
|
||||
def _set_queue(self):
|
||||
self.queue = self.queue_class(timeout=self.queue_timeout)
|
||||
|
||||
def _set_adapter_manager(self):
|
||||
AdapterManager(
|
||||
ble=self._ble,
|
||||
enable_ble_code=self.enable_ble_code,
|
||||
runtime_permissions=self.runtime_permissions,
|
||||
).install(self)
|
||||
|
||||
def _check_runtime_permissions(self):
|
||||
return True
|
||||
|
||||
def _request_runtime_permissions(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def adapter(self) -> Optional["android.bluetooth.BluetoothAdapter"]:
|
||||
"""Local device Bluetooth adapter.
|
||||
Could be `None` if adapter is not enabled or access to the adapter is not granted yet.
|
||||
|
||||
:type: `BluetoothAdapter <https://developer.android.com/reference/android/bluetooth/BluetoothAdapter>`_
|
||||
`Java object <https://pyjnius.readthedocs.io/en/stable/api.html#jnius.JavaClass>`_
|
||||
"""
|
||||
|
||||
@property
|
||||
def gatt(self):
|
||||
"""GATT profile of the connected device
|
||||
|
||||
:type: BluetoothGatt Java object
|
||||
"""
|
||||
return self._ble.getGatt()
|
||||
|
||||
@property
|
||||
def bonded_devices(self):
|
||||
"""List of Java `android.bluetooth.BluetoothDevice` objects of paired BLE devices.
|
||||
|
||||
:type: List[BluetoothDevice]
|
||||
"""
|
||||
return []
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name of the Bluetooth adapter.
|
||||
|
||||
:setter: Set name of the Bluetooth adapter
|
||||
:type: Optional[str]
|
||||
"""
|
||||
adapter = self.adapter
|
||||
return adapter and adapter.getName()
|
||||
|
||||
@name.setter
|
||||
def name(self, value):
|
||||
self._set_name(value)
|
||||
|
||||
def _set_name(self, value):
|
||||
pass
|
||||
|
||||
def set_queue_timeout(self, timeout):
|
||||
"""Change the BLE operations queue timeout"""
|
||||
self.queue_timeout = timeout
|
||||
self.queue.set_timeout(timeout)
|
||||
|
||||
def start_scan(
|
||||
self,
|
||||
filters: Optional[List[Filter]] = None,
|
||||
settings: Optional[ScanSettingsBuilder] = None,
|
||||
):
|
||||
"""Start a scan for devices.
|
||||
The status of the scan start are reported with
|
||||
:func:`scan_started <on_scan_started>` event.
|
||||
|
||||
:param filters: list of filters to restrict scan results.
|
||||
Advertising record is considered matching the filters
|
||||
if it matches any of the :class:`able.filters.Filter` in the list.
|
||||
:param settings: scan settings
|
||||
"""
|
||||
pass
|
||||
|
||||
def stop_scan(self):
|
||||
"""Stop the ongoing scan for devices."""
|
||||
pass
|
||||
|
||||
def connect_by_device_address(self, address: str, autoconnect: bool = False):
|
||||
"""Connect to GATT Server of the device with a given Bluetooth hardware address, without scanning.
|
||||
|
||||
:param address: Bluetooth hardware address string in "XX:XX:XX:XX:XX:XX" format
|
||||
:param autoconnect: If True, automatically reconnects when available.
|
||||
False = direct connect (default).
|
||||
:raises:
|
||||
ValueError: if `address` is not a valid Bluetooth address
|
||||
"""
|
||||
pass
|
||||
|
||||
def connect_gatt(self, device, autoconnect: bool = False):
|
||||
"""Connect to GATT Server hosted by device
|
||||
|
||||
:param device: BluetoothDevice Java object
|
||||
:param autoconnect: If True, automatically reconnects when available.
|
||||
False = direct connect (default).
|
||||
"""
|
||||
self._ble.connectGatt(device, autoconnect)
|
||||
|
||||
def close_gatt(self):
|
||||
"""Close current GATT client"""
|
||||
self._ble.closeGatt()
|
||||
|
||||
def discover_services(self):
|
||||
"""Discovers services offered by a remote device.
|
||||
The status of the discovery reported with
|
||||
:func:`services <on_services>` event.
|
||||
|
||||
:return: true, if the remote services discovery has been started
|
||||
"""
|
||||
return self.gatt.discoverServices()
|
||||
|
||||
def enable_notifications(self, characteristic, enable=True, indication=False):
|
||||
"""Enable/disable notifications or indications for a given characteristic
|
||||
|
||||
:param characteristic: BluetoothGattCharacteristic Java object
|
||||
:param enable: enable notifications if True, else disable notifications
|
||||
:param indication: handle indications instead of notifications
|
||||
:return: True, if the operation was initiated successfully
|
||||
"""
|
||||
return True
|
||||
|
||||
@ble_task
|
||||
def write_descriptor(self, descriptor, value):
|
||||
"""Set and write the value of a given descriptor to the associated
|
||||
remote device
|
||||
|
||||
:param descriptor: BluetoothGattDescriptor Java object
|
||||
:param value: value to write
|
||||
"""
|
||||
if not descriptor.setValue(force_convertible_to_java_array(value)):
|
||||
Logger.error("Error on set descriptor value")
|
||||
return
|
||||
if not self.gatt.writeDescriptor(descriptor):
|
||||
Logger.error("Error on descriptor write")
|
||||
|
||||
@ble_task
|
||||
def write_characteristic(
|
||||
self, characteristic, value, write_type: Optional[WriteType] = None
|
||||
):
|
||||
"""Write a given characteristic value to the associated remote device
|
||||
|
||||
:param characteristic: BluetoothGattCharacteristic Java object
|
||||
:param value: value to write
|
||||
:param write_type: specific write type to set for the characteristic
|
||||
"""
|
||||
self._ble.writeCharacteristic(
|
||||
characteristic, force_convertible_to_java_array(value), int(write_type or 0)
|
||||
)
|
||||
|
||||
@ble_task
|
||||
def read_characteristic(self, characteristic):
|
||||
"""Read a given characteristic from the associated remote device
|
||||
|
||||
:param characteristic: BluetoothGattCharacteristic Java object
|
||||
"""
|
||||
self._ble.readCharacteristic(characteristic)
|
||||
|
||||
@ble_task
|
||||
def update_rssi(self):
|
||||
"""Triggers an update for the RSSI from the associated remote device"""
|
||||
self._ble.readRemoteRssi()
|
||||
|
||||
@ble_task
|
||||
def request_mtu(self, mtu: int):
|
||||
"""Request to change the ATT Maximum Transmission Unit value
|
||||
|
||||
:param value: new MTU size
|
||||
"""
|
||||
self.gatt.requestMtu(mtu)
|
||||
|
||||
def on_error(self, msg):
|
||||
"""Error handler
|
||||
|
||||
:param msg: error message
|
||||
"""
|
||||
self._ble = BLEError(msg) # Exception for calls from another threads
|
||||
raise Exception(msg)
|
||||
|
||||
@ble_task_done
|
||||
def on_gatt_release(self):
|
||||
"""`gatt_release` event handler.
|
||||
Event is dispatched at every read/write completed operation
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_scan_started(self, success):
|
||||
"""`scan_started` event handler
|
||||
|
||||
:param success: true, if scan was started successfully
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_scan_completed(self):
|
||||
"""`scan_completed` event handler"""
|
||||
pass
|
||||
|
||||
def on_device(self, device, rssi, advertisement):
|
||||
"""`device` event handler.
|
||||
Event is dispatched when device is found during a scan.
|
||||
|
||||
:param device: BluetoothDevice Java object
|
||||
:param rssi: the RSSI value for the remote device
|
||||
:param advertisement: :class:`Advertisement` data record
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_connection_state_change(self, status, state):
|
||||
"""`connection_state_change` event handler
|
||||
|
||||
:param status: status of the operation,
|
||||
`GATT_SUCCESS` if the operation succeeds
|
||||
:param state: STATE_CONNECTED or STATE_DISCONNECTED
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_bluetooth_adapter_state_change(self, state):
|
||||
"""`bluetooth_adapter_state_change` event handler
|
||||
Allows the user to detect when bluetooth adapter is turned on/off.
|
||||
|
||||
:param state: STATE_OFF, STATE_TURNING_OFF, STATE_ON, STATE_TURNING_ON
|
||||
"""
|
||||
|
||||
def on_services(self, services, status):
|
||||
"""`services` event handler
|
||||
|
||||
:param services: :class:`Services` dict filled with discovered
|
||||
characteristics
|
||||
(BluetoothGattCharacteristic Java objects)
|
||||
:param status: status of the operation,
|
||||
`GATT_SUCCESS` if the operation succeeds
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_characteristic_changed(self, characteristic):
|
||||
"""`characteristic_changed` event handler
|
||||
|
||||
:param characteristic: BluetoothGattCharacteristic Java object
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_characteristic_read(self, characteristic, status):
|
||||
"""`characteristic_read` event handler
|
||||
|
||||
:param characteristic: BluetoothGattCharacteristic Java object
|
||||
:param status: status of the operation,
|
||||
`GATT_SUCCESS` if the operation succeeds
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_characteristic_write(self, characteristic, status):
|
||||
"""`characteristic_write` event handler
|
||||
|
||||
:param characteristic: BluetoothGattCharacteristic Java object
|
||||
:param status: status of the operation,
|
||||
`GATT_SUCCESS` if the operation succeeds
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_descriptor_read(self, descriptor, status):
|
||||
"""`descriptor_read` event handler
|
||||
|
||||
:param descriptor: BluetoothGattDescriptor Java object
|
||||
:param status: status of the operation,
|
||||
`GATT_SUCCESS` if the operation succeeds
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_descriptor_write(self, descriptor, status):
|
||||
"""`descriptor_write` event handler
|
||||
|
||||
:param descriptor: BluetoothGattDescriptor Java object
|
||||
:param status: status of the operation,
|
||||
`GATT_SUCCESS` if the operation succeeds
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_rssi_updated(self, rssi, status):
|
||||
"""`onReadRemoteRssi` event handler.
|
||||
Event is dispatched at every RSSI update completed operation,
|
||||
reporting a RSSI value for a remote device connection.
|
||||
|
||||
:param rssi: integer containing RSSI value in dBm
|
||||
:param status: status of the operation,
|
||||
`GATT_SUCCESS` if the operation succeeds
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_mtu_changed(self, mtu, status):
|
||||
"""`onMtuChanged` event handler
|
||||
Event is dispatched when MTU for a remote device has changed,
|
||||
reporting a new MTU size.
|
||||
|
||||
:param mtu: integer containing the new MTU size
|
||||
:param status: status of the operation,
|
||||
`GATT_SUCCESS` if the MTU has been changed successfully
|
||||
"""
|
||||
pass
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
"""BLE scanning filters,
|
||||
wrappers for Java class `android.bluetooth.le.ScanFilter.Builder`
|
||||
https://developer.android.com/reference/android/bluetooth/le/ScanFilter.Builder
|
||||
"""
|
||||
from abc import abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Union
|
||||
import uuid
|
||||
|
||||
from jnius import autoclass
|
||||
|
||||
ParcelUuid = autoclass('android.os.ParcelUuid')
|
||||
BluetoothAdapter = autoclass('android.bluetooth.BluetoothAdapter')
|
||||
ScanFilter = autoclass('android.bluetooth.le.ScanFilter')
|
||||
ScanFilterBuilder = autoclass('android.bluetooth.le.ScanFilter$Builder')
|
||||
|
||||
|
||||
@dataclass
|
||||
class Filter:
|
||||
"""Base class for BLE scanning fiters.
|
||||
|
||||
>>> # Filters of different kinds could be ANDed to set multiple conditions.
|
||||
>>> # Both device name and address required:
|
||||
>>> combined_filter = DeviceNameFilter("Example") & DeviceAddressFilter("01:02:03:AB:CD:EF")
|
||||
|
||||
>>> DeviceNameFilter("Example1") & DeviceNameFilter("Example2")
|
||||
Traceback (most recent call last):
|
||||
ValueError: cannot combine filters of the same type
|
||||
"""
|
||||
|
||||
def __post_init__(self):
|
||||
self.filters = [self]
|
||||
|
||||
def __and__(self, other):
|
||||
if type(self) in (type(f) for f in other.filters):
|
||||
raise ValueError('cannot combine filters of the same type')
|
||||
self.filters.extend(other.filters)
|
||||
return self
|
||||
|
||||
def build(self):
|
||||
builder = ScanFilterBuilder()
|
||||
for scan_filter in self.filters:
|
||||
scan_filter.filter(builder)
|
||||
return builder.build()
|
||||
|
||||
@abstractmethod
|
||||
def filter(self, builder):
|
||||
pass
|
||||
|
||||
|
||||
class EmptyFilter(Filter):
|
||||
"""Filter with no restrictions."""
|
||||
|
||||
def filter(self, builder):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeviceAddressFilter(Filter):
|
||||
"""Set filter on device address.
|
||||
Uses Java method `ScanFilter.Builder.setDeviceAddress`.
|
||||
|
||||
:param address: Address in the format of "01:02:03:AB:CD:EF"
|
||||
|
||||
>>> DeviceAddressFilter("01:02:03:AB:CD:EF")
|
||||
DeviceAddressFilter(address='01:02:03:AB:CD:EF')
|
||||
"""
|
||||
address: str
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if not BluetoothAdapter.checkBluetoothAddress(str(self.address)):
|
||||
raise ValueError(f"{self.address} is not a valid Bluetooth address")
|
||||
|
||||
def filter(self, builder):
|
||||
builder.setDeviceAddress(str(self.address))
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeviceNameFilter(Filter):
|
||||
"""Set filter on device name.
|
||||
Uses Java method `ScanFilter.Builder.setDeviceName`.
|
||||
|
||||
:param name: Device name
|
||||
"""
|
||||
name: str
|
||||
|
||||
def filter(self, builder):
|
||||
builder.setDeviceName(str(self.name))
|
||||
|
||||
|
||||
@dataclass
|
||||
class ManufacturerDataFilter(Filter):
|
||||
"""Set filter on manufacture data.
|
||||
Uses Java method `ScanFilter.Builder.setManufacturerData`.
|
||||
|
||||
:param id: Manufacturer ID
|
||||
:param data: Manufacturer specific data
|
||||
:param mask: bit mask for partial filtration of the `data`. For any bit in the mask,
|
||||
set it to 1 if it needs to match the one in manufacturer data,
|
||||
otherwise set it to 0 to ignore that bit.
|
||||
|
||||
|
||||
>>> # Filter by just ID, ignoring the data:
|
||||
>>> ManufacturerDataFilter(0x0AD0, [])
|
||||
ManufacturerDataFilter(id=2768, data=[], mask=None)
|
||||
|
||||
>>> ManufacturerDataFilter(0x0AD0, [0x2, 0x15, 0x8d])
|
||||
ManufacturerDataFilter(id=2768, data=[2, 21, 141], mask=None)
|
||||
|
||||
>>> # With mask set to ignore the second data byte:
|
||||
>>> ManufacturerDataFilter(0x0AD0, [0x2, 0, 0x8d], [0xff, 0, 0xff])
|
||||
ManufacturerDataFilter(id=2768, data=[2, 0, 141], mask=[255, 0, 255])
|
||||
|
||||
>>> ManufacturerDataFilter(0x0AD0, [0x2, 21, 0x8d], [0xff])
|
||||
Traceback (most recent call last):
|
||||
ValueError: mask is shorter than the data
|
||||
"""
|
||||
id: int
|
||||
data: Union[list, tuple, bytes, bytearray]
|
||||
mask: List[int] = field(default_factory=lambda: None)
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if self.mask and len(self.mask) < len(self.data):
|
||||
raise ValueError('mask is shorter than the data')
|
||||
|
||||
def filter(self, builder):
|
||||
if self.mask:
|
||||
builder.setManufacturerData(self.id, self.data, self.mask)
|
||||
else:
|
||||
builder.setManufacturerData(self.id, self.data)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceDataFilter(Filter):
|
||||
"""Set filter on service data.
|
||||
Uses Java method `ScanFilter.Builder.setServiceData`.
|
||||
|
||||
:param uid: UUID of the service in the format of
|
||||
"0000180f-0000-1000-8000-00805f9b34fb"
|
||||
:param data: service data
|
||||
:param mask: bit mask for partial filtration of the `data`. For any bit in the mask,
|
||||
set it to 1 if it needs to match the one in service data,
|
||||
otherwise set it to 0 to ignore that bit.
|
||||
|
||||
>>> ServiceDataFilter("0000180f-0000-1000-8000-00805f9b34fb", [])
|
||||
ServiceDataFilter(uid='0000180f-0000-1000-8000-00805f9b34fb', data=[], mask=None)
|
||||
|
||||
>>> # With mask set to ignore the first data byte:
|
||||
>>> ServiceDataFilter("0000180f-0000-1000-8000-00805f9b34fb", [0, 0x11], [0, 0xff])
|
||||
ServiceDataFilter(uid='0000180f-0000-1000-8000-00805f9b34fb', data=[0, 17], mask=[0, 255])
|
||||
|
||||
>>> ServiceDataFilter("0000180f", [])
|
||||
Traceback (most recent call last):
|
||||
ValueError: badly formed hexadecimal UUID string
|
||||
|
||||
>>> ServiceDataFilter("0000180f-0000-1000-8000-00805f9b34fb", [0x12, 0x34], [0xff])
|
||||
Traceback (most recent call last):
|
||||
ValueError: mask is shorter than the data
|
||||
"""
|
||||
uid: str
|
||||
data: Union[list, tuple, bytes, bytearray]
|
||||
mask: List[int] = field(default_factory=lambda: None)
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
# validate UUID value
|
||||
uuid.UUID(self.uid)
|
||||
if self.mask and len(self.mask) < len(self.data):
|
||||
raise ValueError('mask is shorter than the data')
|
||||
|
||||
def filter(self, builder):
|
||||
uid = ParcelUuid.fromString(self.uid)
|
||||
if self.mask:
|
||||
builder.setServiceData(uid, self.data, self.mask)
|
||||
else:
|
||||
builder.setServiceData(uid, self.data)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceSolicitationFilter(Filter):
|
||||
"""Set filter on service solicitation uuid.
|
||||
Uses Java method `ScanFilter.Builder.setServiceSolicitation`.
|
||||
|
||||
:param uid: UUID of the service in the format of
|
||||
"0000180f-0000-1000-8000-00805f9b34fb"
|
||||
"""
|
||||
uid: str
|
||||
|
||||
def filter(self, builder):
|
||||
uid = ParcelUuid.fromString(self.uid)
|
||||
builder.setServiceSolicitation(uid)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServiceUUIDFilter(Filter):
|
||||
"""Set filter on service uuid.
|
||||
Uses Java method `ScanFilter.Builder.setServiceUuid`.
|
||||
|
||||
:param uid: UUID of the service in the format of
|
||||
"0000180f-0000-1000-8000-00805f9b34fb"
|
||||
:mask: bit mask for partial filtration of the UUID, in the format of
|
||||
"ffffffff-0000-0000-0000-ffffffffffff". Set any bit in the mask
|
||||
to 1 to indicate a match is needed for the bit in `uid`,
|
||||
and 0 to ignore that bit.
|
||||
|
||||
>>> ServiceUUIDFilter('16fe0d00-c111-11e3-b8c8-0002a5d5c51b')
|
||||
ServiceUUIDFilter(uid='16fe0d00-c111-11e3-b8c8-0002a5d5c51b', mask=None)
|
||||
|
||||
>>> ServiceUUIDFilter(
|
||||
... '16fe0d00-c111-11e3-b8c8-0002a5d5c51b',
|
||||
... 'ffffffff-0000-0000-0000-000000000000'
|
||||
... ) #doctest: +ELLIPSIS
|
||||
ServiceUUIDFilter(uid='16fe0d00-...', mask='ffffffff-...')
|
||||
|
||||
>>> ServiceUUIDFilter('123')
|
||||
Traceback (most recent call last):
|
||||
ValueError: badly formed hexadecimal UUID string
|
||||
"""
|
||||
uid: str
|
||||
mask: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
# validate UUID values
|
||||
uuid.UUID(self.uid)
|
||||
if self.mask:
|
||||
uuid.UUID(self.mask)
|
||||
|
||||
def filter(self, builder):
|
||||
uid = ParcelUuid.fromString(self.uid)
|
||||
if self.mask:
|
||||
mask = ParcelUuid.fromString(self.mask)
|
||||
builder.setServiceUuid(uid, mask)
|
||||
else:
|
||||
builder.setServiceUuid(uid)
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
"""Before executing, all :class:`BluetoothDispatcher <able.BluetoothDispatcher>` methods that requires Bluetooth adapter
|
||||
(`start_scan`, `connect_by_device_address`, `enable_notifications`, `adapter` property ...),
|
||||
are asking the user to:
|
||||
|
||||
#. grant runtime permissions,
|
||||
#. turn on Bluetooth adapter.
|
||||
|
||||
The list of requested runtime permissions varies depending on the level of the target Android API level:
|
||||
|
||||
* target API level <=30: ACCESS_FINE_LOCATION - to obtain BLE scan results
|
||||
* target API level >= 31:
|
||||
|
||||
* BLUETOOTH_CONNECT - to enable adapter and to connect to devices
|
||||
* BLUETOOTH_SCAN - to start the scan
|
||||
* ACCESS_FINE_LOCATION - to detect beacons during the scan
|
||||
* BLUETOOTH_ADVERTISE - to be able to advertise to nearby Bluetooth devices
|
||||
|
||||
Requested permissions list can be changed with the `BluetoothDispatcher.runtime_permissions` parameter.
|
||||
"""
|
||||
from jnius import autoclass
|
||||
|
||||
SDK_INT = int(autoclass("android.os.Build$VERSION").SDK_INT)
|
||||
|
||||
|
||||
class Permission:
|
||||
"""
|
||||
String constants values for BLE-related permissions.
|
||||
https://developer.android.com/reference/android/Manifest.permission
|
||||
"""
|
||||
|
||||
ACCESS_BACKGROUND_LOCATION = "android.permission.ACCESS_BACKGROUND_LOCATION"
|
||||
ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION"
|
||||
BLUETOOTH_ADVERTISE = "android.permission.BLUETOOTH_ADVERTISE"
|
||||
BLUETOOTH_CONNECT = "android.permission.BLUETOOTH_CONNECT"
|
||||
BLUETOOTH_SCAN = "android.permission.BLUETOOTH_SCAN"
|
||||
|
||||
|
||||
if SDK_INT >= 31:
|
||||
# API level 31 (Android 12) introduces new permissions
|
||||
DEFAULT_RUNTIME_PERMISSIONS = [
|
||||
Permission.BLUETOOTH_ADVERTISE,
|
||||
Permission.BLUETOOTH_CONNECT,
|
||||
Permission.BLUETOOTH_SCAN,
|
||||
# ACCESS_FINE_LOCATION is not mandatory for scan,
|
||||
# but required to discover beacons
|
||||
Permission.ACCESS_FINE_LOCATION,
|
||||
]
|
||||
else:
|
||||
# For API levels 29-30,
|
||||
# ACCESS_FINE_LOCATION permission is needed to obtain BLE scan results
|
||||
DEFAULT_RUNTIME_PERMISSIONS = [
|
||||
Permission.ACCESS_FINE_LOCATION,
|
||||
]
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
import threading
|
||||
from functools import wraps, partial
|
||||
try:
|
||||
from queue import Empty, Queue
|
||||
except ImportError:
|
||||
from Queue import Empty, Queue
|
||||
from kivy.clock import Clock
|
||||
from kivy.logger import Logger
|
||||
|
||||
|
||||
def ble_task(method):
|
||||
"""
|
||||
Enque method
|
||||
"""
|
||||
@wraps(method)
|
||||
def wrapper(obj, *args, **kwargs):
|
||||
task = partial(method, obj, *args, **kwargs)
|
||||
obj.queue.enque(task)
|
||||
return wrapper
|
||||
|
||||
|
||||
def ble_task_done(method):
|
||||
@wraps(method)
|
||||
def wrapper(obj, *args, **kwargs):
|
||||
obj.queue.done(*args, **kwargs)
|
||||
method(obj, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def with_lock(method):
|
||||
@wraps(method)
|
||||
def wrapped(obj, *args, **kwargs):
|
||||
locked = obj.lock.acquire(False)
|
||||
if locked:
|
||||
try:
|
||||
return method(obj, *args, **kwargs)
|
||||
finally:
|
||||
obj.lock.release()
|
||||
return wrapped
|
||||
|
||||
|
||||
class BLEQueue(object):
|
||||
|
||||
def __init__(self, timeout=0):
|
||||
self.lock = threading.Lock()
|
||||
self.ready = True
|
||||
self.queue = Queue()
|
||||
self.set_timeout(timeout)
|
||||
|
||||
def set_timeout(self, timeout):
|
||||
Logger.debug("set queue timeout to {}".format(timeout))
|
||||
self.timeout = timeout
|
||||
self.timeout_event = Clock.schedule_once(
|
||||
self.on_timeout, self.timeout or 0)
|
||||
self.timeout_event.cancel()
|
||||
|
||||
def enque(self, task):
|
||||
queue = self.queue
|
||||
if self.timeout == 0:
|
||||
self.execute_task(task)
|
||||
else:
|
||||
queue.put_nowait(task)
|
||||
self.execute_next()
|
||||
|
||||
@with_lock
|
||||
def execute_next(self, ready=False):
|
||||
if ready:
|
||||
self.ready = True
|
||||
elif not self.ready:
|
||||
return
|
||||
try:
|
||||
task = self.queue.get_nowait()
|
||||
except Empty:
|
||||
return
|
||||
self.ready = False
|
||||
if task is not None:
|
||||
self.execute_task(task)
|
||||
|
||||
def done(self, *args, **kwargs):
|
||||
self.timeout_event.cancel()
|
||||
self.ready = True
|
||||
self.execute_next()
|
||||
|
||||
def on_timeout(self, *args, **kwargs):
|
||||
self.done()
|
||||
|
||||
def execute_task(self, task):
|
||||
if self.timeout and self.timeout_event:
|
||||
self.timeout_event()
|
||||
task()
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
"""BLE scanning settings.
|
||||
"""
|
||||
from jnius import autoclass
|
||||
from kivy.utils import platform
|
||||
|
||||
|
||||
if platform != 'android':
|
||||
class ScanSettings:
|
||||
"""PyJNIus wrapper for Java class `android.bluetooth.le.ScanSettings`.
|
||||
https://developer.android.com/reference/android/bluetooth/le/ScanSettings
|
||||
"""
|
||||
|
||||
class ScanSettingsBuilder:
|
||||
"""PyJNIus wrapper for Java class `android.bluetooth.le.ScanSettings.Builder`.
|
||||
https://developer.android.com/reference/android/bluetooth/le/ScanSettings.Builder
|
||||
"""
|
||||
|
||||
else:
|
||||
ScanSettings = autoclass('android.bluetooth.le.ScanSettings')
|
||||
ScanSettingsBuilder = autoclass('android.bluetooth.le.ScanSettings$Builder')
|
||||
|
|
@ -1,283 +0,0 @@
|
|||
package org.able;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothManager;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCallback;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGattDescriptor;
|
||||
import android.bluetooth.BluetoothGattService;
|
||||
import android.bluetooth.le.BluetoothLeScanner;
|
||||
import android.bluetooth.le.ScanCallback;
|
||||
import android.bluetooth.le.ScanResult;
|
||||
|
||||
import android.bluetooth.le.ScanFilter;
|
||||
import android.bluetooth.le.ScanSettings;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import java.util.List;
|
||||
import org.kivy.android.PythonActivity;
|
||||
import org.kivy.android.PythonService;
|
||||
import org.able.PythonBluetooth;
|
||||
|
||||
|
||||
public class BLE {
|
||||
private String TAG = "BLE-python";
|
||||
private PythonBluetooth mPython;
|
||||
private Context mContext;
|
||||
private BluetoothAdapter mBluetoothAdapter;
|
||||
private BluetoothLeScanner mBluetoothLeScanner;
|
||||
private BluetoothGatt mBluetoothGatt;
|
||||
private List<BluetoothGattService> mBluetoothGattServices;
|
||||
private boolean mScanning;
|
||||
private boolean mIsServiceContext = false;
|
||||
|
||||
public void showError(final String msg) {
|
||||
Log.e(TAG, msg);
|
||||
if (!mIsServiceContext) { PythonActivity.mActivity.toastError(TAG + " error. " + msg); }
|
||||
mPython.on_error(msg);
|
||||
}
|
||||
|
||||
public BLE(PythonBluetooth python) {
|
||||
mPython = python;
|
||||
mContext = (Context) PythonActivity.mActivity;
|
||||
mBluetoothGatt = null;
|
||||
|
||||
if (mContext == null) {
|
||||
Log.d(TAG, "Service context detected");
|
||||
mIsServiceContext = true;
|
||||
mContext = (Context) PythonService.mService;
|
||||
}
|
||||
|
||||
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
|
||||
showError("Device does not support Bluetooth Low Energy.");
|
||||
return;
|
||||
}
|
||||
|
||||
final BluetoothManager bluetoothManager =
|
||||
(BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE);
|
||||
mBluetoothAdapter = bluetoothManager.getAdapter();
|
||||
mContext.registerReceiver(mReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
|
||||
}
|
||||
|
||||
public BluetoothAdapter getAdapter(int EnableBtCode) {
|
||||
if (mBluetoothAdapter == null) {
|
||||
showError("Device do not support Bluetooth Low Energy.");
|
||||
return null;
|
||||
}
|
||||
if (!mBluetoothAdapter.isEnabled()) {
|
||||
if (mIsServiceContext) {
|
||||
showError("BLE adapter is not enabled");
|
||||
} else {
|
||||
Log.d(TAG, "BLE adapter is not enabled");
|
||||
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
|
||||
PythonActivity.mActivity.startActivityForResult(enableBtIntent, EnableBtCode);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return mBluetoothAdapter;
|
||||
}
|
||||
|
||||
public BluetoothGatt getGatt() {
|
||||
return mBluetoothGatt;
|
||||
}
|
||||
|
||||
public void startScan(int EnableBtCode,
|
||||
List<ScanFilter> filters,
|
||||
ScanSettings settings) {
|
||||
Log.d(TAG, "startScan");
|
||||
BluetoothAdapter adapter = getAdapter(EnableBtCode);
|
||||
if (adapter != null) {
|
||||
Log.d(TAG, "BLE adapter is ready for scan");
|
||||
if (mBluetoothLeScanner == null) {
|
||||
mBluetoothLeScanner = adapter.getBluetoothLeScanner();
|
||||
}
|
||||
if (mBluetoothLeScanner != null) {
|
||||
mScanning = false;
|
||||
mBluetoothLeScanner.startScan(filters, settings, mScanCallback);
|
||||
} else {
|
||||
showError("Could not get BLE Scanner object.");
|
||||
mPython.on_scan_started(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void stopScan() {
|
||||
if (mBluetoothLeScanner != null) {
|
||||
Log.d(TAG, "stopScan");
|
||||
mBluetoothLeScanner.stopScan(mScanCallback);
|
||||
if (mScanning) {
|
||||
mScanning = false;
|
||||
mPython.on_scan_completed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final ScanCallback mScanCallback =
|
||||
new ScanCallback() {
|
||||
@Override
|
||||
public void onScanResult(final int callbackType, final ScanResult result) {
|
||||
if (!mScanning) {
|
||||
mScanning = true;
|
||||
Log.d(TAG, "BLE scan started successfully");
|
||||
mPython.on_scan_started(true);
|
||||
}
|
||||
if (mIsServiceContext) {
|
||||
mPython.on_scan_result(result);
|
||||
return;
|
||||
}
|
||||
PythonActivity.mActivity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mPython.on_scan_result(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBatchScanResults(List<ScanResult> results) {
|
||||
Log.d(TAG, "onBatchScanResults");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanFailed(int errorCode) {
|
||||
Log.e(TAG, "BLE Scan failed, error code:" + errorCode);
|
||||
mPython.on_scan_started(false);
|
||||
}
|
||||
};
|
||||
|
||||
public void connectGatt(BluetoothDevice device) {
|
||||
connectGatt(device, false);
|
||||
}
|
||||
|
||||
public void connectGatt(BluetoothDevice device, boolean autoConnect) {
|
||||
Log.d(TAG, "connectGatt");
|
||||
if (mBluetoothGatt == null) {
|
||||
mBluetoothGatt = device.connectGatt(mContext, autoConnect, mGattCallback, BluetoothDevice.TRANSPORT_LE);
|
||||
} else {
|
||||
Log.d(TAG, "BluetoothGatt object exists, use either closeGatt() to close Gatt or BluetoothGatt.connect() to re-connect");
|
||||
}
|
||||
}
|
||||
|
||||
public void closeGatt() {
|
||||
Log.d(TAG, "closeGatt");
|
||||
if (mBluetoothGatt != null) {
|
||||
mBluetoothGatt.close();
|
||||
mBluetoothGatt = null;
|
||||
}
|
||||
}
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
|
||||
Log.d(TAG, "onReceive - BluetoothAdapter state changed");
|
||||
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
|
||||
mPython.on_bluetooth_adapter_state_change(state);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final BluetoothGattCallback mGattCallback =
|
||||
new BluetoothGattCallback() {
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
|
||||
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
||||
Log.d(TAG, "Connected to GATT server, status:" + status);
|
||||
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
|
||||
Log.d(TAG, "Disconnected from GATT server, status:" + status);
|
||||
}
|
||||
if (mBluetoothGatt == null) {
|
||||
mBluetoothGatt = gatt;
|
||||
}
|
||||
mPython.on_connection_state_change(status, newState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
Log.d(TAG, "onServicesDiscovered - success");
|
||||
mBluetoothGattServices = mBluetoothGatt.getServices();
|
||||
} else {
|
||||
showError("onServicesDiscovered status:" + status);
|
||||
mBluetoothGattServices = null;
|
||||
}
|
||||
mPython.on_services(status, mBluetoothGattServices);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicChanged(BluetoothGatt gatt,
|
||||
BluetoothGattCharacteristic characteristic) {
|
||||
mPython.on_characteristic_changed(characteristic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicRead(BluetoothGatt gatt,
|
||||
BluetoothGattCharacteristic characteristic,
|
||||
int status) {
|
||||
mPython.on_characteristic_read(characteristic, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWrite(BluetoothGatt gatt,
|
||||
BluetoothGattCharacteristic characteristic,
|
||||
int status) {
|
||||
mPython.on_characteristic_write(characteristic, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorRead(BluetoothGatt gatt,
|
||||
BluetoothGattDescriptor descriptor,
|
||||
int status) {
|
||||
mPython.on_descriptor_read(descriptor, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorWrite(BluetoothGatt gatt,
|
||||
BluetoothGattDescriptor descriptor,
|
||||
int status) {
|
||||
mPython.on_descriptor_write(descriptor, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReadRemoteRssi(BluetoothGatt gatt,
|
||||
int rssi, int status) {
|
||||
mPython.on_rssi_updated(rssi, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMtuChanged(BluetoothGatt gatt,
|
||||
int mtu, int status) {
|
||||
Log.d(TAG, String.format("onMtuChanged mtu=%d status=%d", mtu, status));
|
||||
mPython.on_mtu_changed(mtu, status);
|
||||
}
|
||||
};
|
||||
|
||||
public boolean writeCharacteristic(BluetoothGattCharacteristic characteristic, byte[] data, int writeType) {
|
||||
if (characteristic.setValue(data)) {
|
||||
if (writeType != 0) {
|
||||
characteristic.setWriteType(writeType);
|
||||
}
|
||||
return mBluetoothGatt.writeCharacteristic(characteristic);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean readCharacteristic(BluetoothGattCharacteristic characteristic) {
|
||||
return mBluetoothGatt.readCharacteristic(characteristic);
|
||||
}
|
||||
|
||||
public boolean readRemoteRssi() {
|
||||
return mBluetoothGatt.readRemoteRssi();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
package org.able;
|
||||
|
||||
import android.bluetooth.le.AdvertisingSet;
|
||||
import android.bluetooth.le.AdvertisingSetCallback;
|
||||
import org.able.PythonBluetoothAdvertiser;
|
||||
import android.util.Log;
|
||||
|
||||
|
||||
public class BLEAdvertiser {
|
||||
private String TAG = "BLE-python";
|
||||
private PythonBluetoothAdvertiser mPython;
|
||||
public AdvertisingSetCallback mCallbackSet;
|
||||
|
||||
public BLEAdvertiser(PythonBluetoothAdvertiser python) {
|
||||
mPython = python;
|
||||
}
|
||||
|
||||
public AdvertisingSetCallback createCallback() {
|
||||
mCallbackSet = new AdvertisingSetCallback() {
|
||||
@Override
|
||||
public void onAdvertisingSetStarted(AdvertisingSet advertisingSet, int txPower, int status) {
|
||||
Log.d(TAG, "onAdvertisingSetStarted, status:" + status);
|
||||
mPython.on_advertising_started(advertisingSet, txPower, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdvertisingSetStopped(AdvertisingSet advertisingSet) {
|
||||
Log.d(TAG, "onAdvertisingSetStopped");
|
||||
mCallbackSet = null;
|
||||
mPython.on_advertising_stopped(advertisingSet);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onAdvertisingEnabled(AdvertisingSet advertisingSet, boolean enable, int status) {
|
||||
Log.d(TAG, "onAdvertisingEnabled, enable:" + enable + "status:" + status);
|
||||
mPython.on_advertising_enabled(advertisingSet, enable, status);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onAdvertisingDataSet(AdvertisingSet advertisingSet, int status) {
|
||||
Log.d(TAG, "onAdvertisingDataSet, status:" + status);
|
||||
mPython.on_advertising_data_set(advertisingSet, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanResponseDataSet(AdvertisingSet advertisingSet, int status) {
|
||||
Log.d(TAG, "onScanResponseDataSet, status:" + status);
|
||||
mPython.on_scan_response_data_set(advertisingSet, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdvertisingParametersUpdated(AdvertisingSet advertisingSet, int txPower, int status) {
|
||||
Log.d(TAG, "onAdvertisingParametersUpdated, status:" + status);
|
||||
mPython.on_advertising_parameters_updated(advertisingSet, txPower, status);
|
||||
}
|
||||
};
|
||||
return mCallbackSet;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package org.able;
|
||||
|
||||
import java.util.List;
|
||||
import android.bluetooth.BluetoothGattService;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGattDescriptor;
|
||||
import android.bluetooth.le.ScanResult;
|
||||
|
||||
interface PythonBluetooth
|
||||
{
|
||||
public void on_error(String msg);
|
||||
public void on_scan_started(boolean success);
|
||||
public void on_scan_result(ScanResult result);
|
||||
public void on_scan_completed();
|
||||
public void on_services(int status, List<BluetoothGattService> services);
|
||||
public void on_characteristic_changed(BluetoothGattCharacteristic characteristic);
|
||||
public void on_characteristic_read(BluetoothGattCharacteristic characteristic, int status);
|
||||
public void on_characteristic_write(BluetoothGattCharacteristic characteristic, int status);
|
||||
public void on_descriptor_read(BluetoothGattDescriptor descriptor, int status);
|
||||
public void on_descriptor_write(BluetoothGattDescriptor descriptor, int status);
|
||||
public void on_connection_state_change(int status, int state);
|
||||
public void on_bluetooth_adapter_state_change(int state);
|
||||
public void on_rssi_updated(int rssi, int status);
|
||||
public void on_mtu_changed (int mtu, int status);
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package org.able;
|
||||
|
||||
import android.bluetooth.le.AdvertisingSet;
|
||||
|
||||
interface PythonBluetoothAdvertiser
|
||||
{
|
||||
public void on_advertising_started(AdvertisingSet advertisingSet, int txPower, int status);
|
||||
public void on_advertising_stopped(AdvertisingSet advertisingSet);
|
||||
public void on_advertising_enabled(AdvertisingSet advertisingSet, boolean enable, int status);
|
||||
public void on_advertising_data_set(AdvertisingSet advertisingSet, int status);
|
||||
public void on_scan_response_data_set(AdvertisingSet advertisingSet, int status);
|
||||
public void on_advertising_parameters_updated(AdvertisingSet advertisingSet, int txPower, int status);
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
import re
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
class Advertisement(object):
|
||||
"""Advertisement data record parser
|
||||
|
||||
>>> ad = Advertisement([2, 1, 0x6, 6, 255, 82, 83, 95, 82, 48])
|
||||
>>> for data in ad:
|
||||
... data
|
||||
AD(ad_type=1, data=bytearray(b'\\x06'))
|
||||
AD(ad_type=255, data=bytearray(b'RS_R0'))
|
||||
>>> list(ad)[0].ad_type == Advertisement.ad_types.flags
|
||||
True
|
||||
"""
|
||||
|
||||
AD = namedtuple("AD", ['ad_type', 'data'])
|
||||
|
||||
class ad_types:
|
||||
"""
|
||||
Assigned numbers for some of `advertisement data types
|
||||
<https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/>`_.
|
||||
|
||||
flags : "Flags" (0x01)
|
||||
|
||||
complete_local_name : "Complete Local Name" (0x09)
|
||||
|
||||
service_data : "Service Data" (0x16)
|
||||
|
||||
manufacturer_specific_data : "Manufacturer Specific Data" (0xff)
|
||||
"""
|
||||
flags = 0x01
|
||||
complete_local_name = 0x09
|
||||
service_data = 0x16
|
||||
manufacturer_specific_data = 0xff
|
||||
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
def __iter__(self):
|
||||
return Advertisement.parse(self.data)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, data):
|
||||
pos = 0
|
||||
while pos < len(data):
|
||||
length = data[pos]
|
||||
if length < 2:
|
||||
return
|
||||
try:
|
||||
ad_type = data[pos + 1]
|
||||
except IndexError:
|
||||
return
|
||||
next_pos = pos + length + 1
|
||||
if ad_type:
|
||||
segment = slice(pos + 2, next_pos)
|
||||
yield Advertisement.AD(ad_type, bytearray(data[segment]))
|
||||
pos = next_pos
|
||||
|
||||
|
||||
class Services(dict):
|
||||
"""Services dict
|
||||
|
||||
>>> services = Services({'service0': {'c1-aa': 0, 'aa-c2-aa': 1},
|
||||
... 'service1': {'bb-c3-bb': 2}})
|
||||
>>> services.search('c3')
|
||||
2
|
||||
>>> services.search('c4')
|
||||
"""
|
||||
|
||||
def search(self, pattern, flags=re.IGNORECASE):
|
||||
"""Search for characteristic by pattern
|
||||
|
||||
:param pattern: regexp pattern
|
||||
:param flags: regexp flags, re.IGNORECASE by default
|
||||
"""
|
||||
for characteristics in self.values():
|
||||
for uuid, characteristic in characteristics.items():
|
||||
if re.search(pattern, uuid, flags):
|
||||
return characteristic
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
from typing import Any, Union
|
||||
|
||||
|
||||
def force_convertible_to_java_array(
|
||||
value: Any
|
||||
) -> Union[list, tuple, bytes, bytearray]:
|
||||
"""Construct a value that is convertible to a Java array.
|
||||
|
||||
>>> force_convertible_to_java_array([3, 1, 4])
|
||||
[3, 1, 4]
|
||||
>>> force_convertible_to_java_array(['314'])
|
||||
['314']
|
||||
>>> force_convertible_to_java_array('314')
|
||||
b'314'
|
||||
>>> force_convertible_to_java_array(314)
|
||||
[314]
|
||||
>>> force_convertible_to_java_array(0)
|
||||
[0]
|
||||
>>> force_convertible_to_java_array('')
|
||||
[]
|
||||
>>> force_convertible_to_java_array(None)
|
||||
[]
|
||||
>>> force_convertible_to_java_array({})
|
||||
[]
|
||||
"""
|
||||
if isinstance(value, (list, tuple, bytes, bytearray)):
|
||||
return value
|
||||
|
||||
try:
|
||||
return value.encode() or []
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return list(value)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
if value is None:
|
||||
return []
|
||||
|
||||
return [value]
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
"""Package version.
|
||||
This file is filled with actual value during the PyPI package build.
|
||||
Development version is always "0.0.0".
|
||||
"""
|
||||
__version__ = '0.0.0'
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# User-friendly check for sphinx-build
|
||||
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
||||
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
|
||||
endif
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " xml to make Docutils-native XML files"
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ABLE.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ABLE.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/ABLE"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ABLE"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
latexpdfja:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||
|
||||
pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
API
|
||||
---
|
||||
|
||||
.. automodule:: able
|
||||
|
||||
Client
|
||||
^^^^^^
|
||||
|
||||
BluetoothDispatcher
|
||||
"""""""""""""""""""
|
||||
|
||||
.. autoclass:: BluetoothDispatcher
|
||||
:members: adapter,
|
||||
gatt,
|
||||
bonded_devices,
|
||||
name,
|
||||
set_queue_timeout,
|
||||
start_scan,
|
||||
stop_scan,
|
||||
connect_by_device_address,
|
||||
connect_gatt,
|
||||
close_gatt,
|
||||
discover_services,
|
||||
enable_notifications,
|
||||
write_descriptor,
|
||||
write_characteristic,
|
||||
read_characteristic,
|
||||
update_rssi,
|
||||
request_mtu,
|
||||
on_error,
|
||||
on_gatt_release,
|
||||
on_scan_started,
|
||||
on_scan_completed,
|
||||
on_device,
|
||||
on_bluetooth_adapter_state_changeable,
|
||||
on_connection_state_change,
|
||||
on_services,
|
||||
on_characteristic_changed,
|
||||
on_characteristic_read,
|
||||
on_characteristic_write,
|
||||
on_descriptor_read,
|
||||
on_descriptor_write,
|
||||
on_rssi_updated,
|
||||
on_mtu_changed,
|
||||
|
||||
Decorators
|
||||
""""""""""
|
||||
|
||||
.. autofunction:: require_bluetooth_enabled
|
||||
|
||||
|
||||
Advertisement
|
||||
"""""""""""""
|
||||
|
||||
.. autoclass:: Advertisement
|
||||
|
||||
.. autoclass:: able::Advertisement.ad_types
|
||||
|
||||
Services
|
||||
""""""""
|
||||
|
||||
.. autoclass:: Services
|
||||
:members:
|
||||
|
||||
Constants
|
||||
"""""""""
|
||||
|
||||
.. autodata:: GATT_SUCCESS
|
||||
.. autodata:: STATE_CONNECTED
|
||||
.. autodata:: STATE_DISCONNECTED
|
||||
.. autoclass:: AdapterState
|
||||
:members:
|
||||
:member-order: bysource
|
||||
.. autoclass:: WriteType
|
||||
:members:
|
||||
|
||||
Permissions
|
||||
^^^^^^^^^^^
|
||||
|
||||
.. automodule:: able.permissions
|
||||
.. automodule:: able
|
||||
.. autoclass:: Permission
|
||||
:members:
|
||||
:undoc-members:
|
||||
:member-order: bysource
|
||||
|
||||
|
||||
Scan settings
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
.. automodule:: able.scan_settings
|
||||
.. autoclass:: ScanSettingsBuilder
|
||||
.. autoclass:: ScanSettings
|
||||
|
||||
|
||||
>>> settings = ScanSettingsBuilder() \
|
||||
... .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) \
|
||||
... .setCallbackType(
|
||||
... ScanSettings.CALLBACK_TYPE_FIRST_MATCH |
|
||||
... ScanSettings.CALLBACK_TYPE_MATCH_LOST
|
||||
... )
|
||||
|
||||
|
||||
Scan filters
|
||||
^^^^^^^^^^^^
|
||||
|
||||
.. automodule:: able.filters
|
||||
:members:
|
||||
:member-order: bysource
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Advertising
|
||||
^^^^^^^^^^^
|
||||
|
||||
.. automodule:: able.advertising
|
||||
|
||||
Advertiser
|
||||
""""""""""
|
||||
.. autoclass:: Advertiser
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
|
||||
Payload
|
||||
"""""""
|
||||
.. autoclass:: AdvertiseData
|
||||
|
||||
.. autoclass:: DeviceName
|
||||
:show-inheritance:
|
||||
.. autoclass:: TXPowerLevel
|
||||
:show-inheritance:
|
||||
.. autoclass:: ServiceUUID
|
||||
:show-inheritance:
|
||||
.. autoclass:: ServiceData
|
||||
:show-inheritance:
|
||||
.. autoclass:: ManufacturerData
|
||||
:show-inheritance:
|
||||
|
||||
Constants
|
||||
"""""""""
|
||||
|
||||
.. autoclass:: Interval
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
.. autoclass:: TXPower
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
.. autoclass:: Status
|
||||
:members:
|
||||
:undoc-members:
|
||||
:member-order: bysource
|
||||
|
|
@ -1,295 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# ABLE documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Apr 16 23:19:55 2017.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.viewcode',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'ABLE'
|
||||
copyright = u'2017, b3b'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.1'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
#keep_warnings = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'ABLEdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'ABLE.tex', u'ABLE Documentation',
|
||||
u'b3b', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'able', u'ABLE Documentation',
|
||||
[u'b3b'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'ABLE', u'ABLE Documentation',
|
||||
u'b3b', 'ABLE', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#texinfo_no_detailmenu = False
|
||||
|
||||
|
||||
# http://stackoverflow.com/questions/28366818/preserve-default-arguments-of-wrapped-decorated-python-function-in-sphinx-document
|
||||
# Monkey-patch functools.wraps
|
||||
import functools
|
||||
|
||||
def no_op_wraps(func):
|
||||
"""Replaces functools.wraps in order to undo wrapping.
|
||||
|
||||
Can be used to preserve the decorated function's signature
|
||||
in the documentation generated by Sphinx.
|
||||
|
||||
"""
|
||||
def wrapper(decorator):
|
||||
return func
|
||||
return wrapper
|
||||
|
||||
functools.wraps = no_op_wraps
|
||||
|
||||
|
||||
# http://docs.readthedocs.io/en/latest/faq.html#i-get-import-errors-on-libraries-that-depend-on-c-modules
|
||||
# I get import errors on libraries that depend on C modules
|
||||
from mock import MagicMock
|
||||
|
||||
class Mock(MagicMock):
|
||||
@classmethod
|
||||
def __getattr__(cls, name):
|
||||
return MagicMock()
|
||||
|
||||
MOCK_MODULES = ['kivy', 'kivy.utils', 'kivy.clock', 'kivy.logger',
|
||||
'kivy.event']
|
||||
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
|
||||
sys.modules['kivy.event'].EventDispatcher = object
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
Usage Examples
|
||||
==============
|
||||
|
||||
Alert
|
||||
-----
|
||||
|
||||
.. literalinclude:: ./examples/alert.py
|
||||
:language: python
|
||||
|
||||
Full example code: `alert <https://github.com/b3b/able/blob/master/examples/alert/>`_
|
||||
|
||||
|
||||
Change MTU
|
||||
----------
|
||||
.. literalinclude:: ./examples/mtu.py
|
||||
:language: python
|
||||
|
||||
|
||||
Scan settings
|
||||
-------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from able import BluetoothDispatcher
|
||||
from able.scan_settings import ScanSettingsBuilder, ScanSettings
|
||||
|
||||
# Use faster detection (more power usage) mode
|
||||
settings = ScanSettingsBuilder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
||||
BluetoothDispatcher().start_scan(settings=settings)
|
||||
|
||||
|
||||
Scan filters
|
||||
------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from able import BluetoothDispatcher
|
||||
from able.filters import (
|
||||
DeviceAddressFilter,
|
||||
DeviceNameFilter,
|
||||
ManufacturerDataFilter,
|
||||
ServiceDataFilter,
|
||||
ServiceUUIDFilter
|
||||
)
|
||||
|
||||
ble = BluetoothDispatcher()
|
||||
|
||||
# Start scanning with the condition that device has one of names: "Device1" or "Device2"
|
||||
ble.start_scan(filters=[DeviceNameFilter("Device1"), DeviceNameFilter("Device2")])
|
||||
ble.stop_scan()
|
||||
|
||||
# Start scanning with the condition that
|
||||
# device advertises "180f" service and one of names: "Device1" or "Device2"
|
||||
ble.start_scan(filters=[
|
||||
ServiceUUIDFilter('0000180f-0000-1000-8000-00805f9b34fb') & DeviceNameFilter("Device1"),
|
||||
ServiceUUIDFilter('0000180f-0000-1000-8000-00805f9b34fb') & DeviceNameFilter("Device2")
|
||||
])
|
||||
|
||||
|
||||
Adapter state
|
||||
-------------
|
||||
|
||||
.. literalinclude:: ./examples/adapter_state_change.py
|
||||
:language: python
|
||||
|
||||
|
||||
Advertising
|
||||
-----------
|
||||
|
||||
Advertise with data and additional (scannable) data
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
.. code-block:: python
|
||||
|
||||
from able import BluetoothDispatcher
|
||||
from able.advertising import (
|
||||
Advertiser,
|
||||
AdvertiseData,
|
||||
ManufacturerData,
|
||||
Interval,
|
||||
ServiceUUID,
|
||||
ServiceData,
|
||||
TXPower,
|
||||
)
|
||||
|
||||
advertiser = Advertiser(
|
||||
ble=BluetoothDispatcher(),
|
||||
data=AdvertiseData(ServiceUUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")),
|
||||
scan_data=AdvertiseData(ManufacturerData(id=0xAABB, data=b"some data")),
|
||||
interval=Interval.MEDIUM,
|
||||
tx_power=TXPower.MEDIUM,
|
||||
)
|
||||
|
||||
advertiser.start()
|
||||
|
||||
|
||||
Set and advertise device name
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from able import BluetoothDispatcher
|
||||
from able.advertising import Advertiser, AdvertiseData, DeviceName
|
||||
|
||||
ble = BluetoothDispatcher()
|
||||
ble.name = "New test device name"
|
||||
|
||||
# There must be a wait and check, it takes time for new name to take effect
|
||||
print(f"New device name is set: {ble.name}")
|
||||
|
||||
Advertiser(
|
||||
ble=ble,
|
||||
data=AdvertiseData(DeviceName())
|
||||
)
|
||||
|
||||
|
||||
Battery service data
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. literalinclude:: ./examples/advertising_battery.py
|
||||
:language: python
|
||||
|
||||
|
||||
Use iBeacon advertising format
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import uuid
|
||||
from able import BluetoothDispatcher
|
||||
from able.advertising import Advertiser, AdvertiseData, ManufacturerData
|
||||
|
||||
|
||||
data = AdvertiseData(
|
||||
ManufacturerData(
|
||||
0x4C, # Apple Manufacturer ID
|
||||
bytes([
|
||||
0x2, # SubType: Custom Manufacturer Data
|
||||
0x15 # Subtype lenth
|
||||
]) +
|
||||
uuid.uuid4().bytes + # UUID of beacon
|
||||
bytes([
|
||||
0, 15, # Major value
|
||||
0, 1, # Minor value
|
||||
10 # RSSI, dBm at 1m
|
||||
]))
|
||||
)
|
||||
|
||||
Advertiser(BluetoothDispatcher(), data).start()
|
||||
|
||||
|
||||
Android Services
|
||||
----------------
|
||||
|
||||
BLE devices scanning service
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
**main.py**
|
||||
|
||||
.. literalinclude:: ./examples/service_scan_main.py
|
||||
:language: python
|
||||
|
||||
**service.py**
|
||||
|
||||
.. literalinclude:: ./examples/service_scan_service.py
|
||||
:language: python
|
||||
|
||||
Full example code: `service_scan <https://github.com/b3b/able/blob/master/examples/service_scan/>`_
|
||||
|
||||
|
||||
Advertising service
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
**main.py**
|
||||
|
||||
.. literalinclude:: ./examples/service_advertise_main.py
|
||||
:language: python
|
||||
|
||||
**service.py**
|
||||
|
||||
.. literalinclude:: ./examples/service_advertise_service.py
|
||||
:language: python
|
||||
|
||||
Full example code: `service_advertise <https://github.com/b3b/able/blob/master/examples/service_advertise/>`_
|
||||
|
||||
|
||||
Connect to multiple devices
|
||||
---------------------------
|
||||
|
||||
.. literalinclude:: ./examples/multi_devices/main.py
|
||||
:language: python
|
||||
|
||||
Full example code: `multi_devices <https://github.com/b3b/able/blob/master/examples/multi_devices/>`_
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
.. include:: ../README.rst
|
||||
.. include:: api.rst
|
||||
.. include:: example.rst
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
"""Detect and log Bluetooth adapter state change."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from kivy.logger import Logger
|
||||
from kivy.uix.widget import Widget
|
||||
|
||||
from able import AdapterState, BluetoothDispatcher
|
||||
|
||||
|
||||
class Dispatcher(BluetoothDispatcher):
|
||||
def on_bluetooth_adapter_state_change(self, state: int):
|
||||
Logger.info(
|
||||
f"Bluetoth adapter state changed to {state} ('{AdapterState(state).name}')."
|
||||
)
|
||||
if state == AdapterState.OFF:
|
||||
Logger.info("Adapter state changed to OFF.")
|
||||
|
||||
|
||||
class StateChangeApp(App):
|
||||
def build(self):
|
||||
Dispatcher()
|
||||
return Widget()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
StateChangeApp.run()
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
"""Advertise battery level, that degrades every second."""
|
||||
from kivy.app import App
|
||||
from kivy.clock import Clock
|
||||
from kivy.uix.label import Label
|
||||
|
||||
from able import BluetoothDispatcher
|
||||
from able import advertising
|
||||
|
||||
# Standard fully-qualified UUID for the Battery Service
|
||||
BATTERY_SERVICE_UUID = "0000180f-0000-1000-8000-00805f9b34fb"
|
||||
|
||||
|
||||
class BatteryAdvertiser(advertising.Advertiser):
|
||||
|
||||
def on_advertising_started(self, advertising_set, tx_power, status):
|
||||
if status == advertising.Status.SUCCESS:
|
||||
print("Advertising is started successfully")
|
||||
else:
|
||||
print(f"Advertising start error status: {status}")
|
||||
|
||||
def on_advertising_stopped(self, advertising_set):
|
||||
print("Advertising stopped")
|
||||
|
||||
|
||||
class BatteryLabel(Label):
|
||||
"""Widget to control advertiser and show current battery level."""
|
||||
|
||||
def __init__(self):
|
||||
self._level = 0
|
||||
super().__init__(text="Waiting for advertising to start...")
|
||||
self.advertiser = BatteryAdvertiser(
|
||||
ble=BluetoothDispatcher(),
|
||||
data=self.construct_data(level=100),
|
||||
interval=advertising.Interval.MIN
|
||||
)
|
||||
self.advertiser.bind(on_advertising_started=self.on_started) # bind to start of advertising
|
||||
self.advertiser.start()
|
||||
|
||||
def on_started(self, advertiser, advertising_set, tx_power, status):
|
||||
if status == advertising.Status.SUCCESS:
|
||||
# Advertising is started - update battery level every second
|
||||
self.clock = Clock.schedule_interval(self.update_level, 1)
|
||||
|
||||
def update_level(self, dt):
|
||||
level = self._level = (self._level - 1) % 101
|
||||
self.text = str(level)
|
||||
|
||||
if level > 0:
|
||||
# Set new advertising data
|
||||
self.advertiser.data = self.construct_data(level)
|
||||
else:
|
||||
self.clock.cancel()
|
||||
# Stop advertising
|
||||
self.advertiser.stop()
|
||||
|
||||
def construct_data(self, level):
|
||||
return advertising.AdvertiseData(
|
||||
advertising.DeviceName(),
|
||||
advertising.TXPowerLevel(),
|
||||
advertising.ServiceData(BATTERY_SERVICE_UUID, [level])
|
||||
)
|
||||
|
||||
|
||||
class BatteryApp(App):
|
||||
|
||||
def build(self):
|
||||
return BatteryLabel()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
BatteryApp().run()
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
[app]
|
||||
title = Alert Mi
|
||||
version = 1.1
|
||||
package.name = alert_mi
|
||||
package.domain = org.kivy
|
||||
source.dir = .
|
||||
source.include_exts = py,png,jpg,kv,atlas
|
||||
|
||||
requirements = python3,kivy,android,able_recipe
|
||||
|
||||
android.accept_sdk_license = True
|
||||
android.permissions =
|
||||
BLUETOOTH,
|
||||
BLUETOOTH_ADMIN,
|
||||
BLUETOOTH_SCAN,
|
||||
BLUETOOTH_CONNECT,
|
||||
BLUETOOTH_ADVERTISE,
|
||||
ACCESS_FINE_LOCATION
|
||||
|
||||
# android.api = 31
|
||||
# android.minapi = 31
|
||||
|
||||
|
||||
[buildozer]
|
||||
warn_on_root = 1
|
||||
# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
|
||||
log_level = 2
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<ErrorMessage>:
|
||||
BoxLayout:
|
||||
orientation: 'vertical'
|
||||
padding: 10
|
||||
spacing: 20
|
||||
Label:
|
||||
size_hint_y: None
|
||||
font_size: '18sp'
|
||||
height: '24sp'
|
||||
text: 'Application has crashed, details: '
|
||||
ScrollView:
|
||||
size_hint: 1, 1
|
||||
TextInput:
|
||||
text: root.message
|
||||
size_hint: 1, None
|
||||
height: self.minimum_height
|
||||
Button:
|
||||
size_hint_y: None
|
||||
height: '40sp'
|
||||
text: 'OK, terminate'
|
||||
on_press: root.dismiss()
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import os
|
||||
import traceback
|
||||
|
||||
from kivy.base import (
|
||||
ExceptionHandler,
|
||||
ExceptionManager,
|
||||
stopTouchApp,
|
||||
)
|
||||
from kivy.properties import StringProperty
|
||||
from kivy.uix.popup import Popup
|
||||
from kivy.lang import Builder
|
||||
from kivy.logger import Logger
|
||||
|
||||
|
||||
Builder.load_file(os.path.join(os.path.dirname(__file__), 'error_message.kv'))
|
||||
|
||||
|
||||
class ErrorMessageOnException(ExceptionHandler):
|
||||
|
||||
def handle_exception(self, exception):
|
||||
Logger.exception('Unhandled Exception catched')
|
||||
message = ErrorMessage(message=traceback.format_exc())
|
||||
|
||||
def raise_exception(*ar2gs, **kwargs):
|
||||
stopTouchApp()
|
||||
raise Exception("Exit due to errors")
|
||||
|
||||
message.bind(on_dismiss=raise_exception)
|
||||
message.open()
|
||||
return ExceptionManager.PASS
|
||||
|
||||
|
||||
class ErrorMessage(Popup):
|
||||
title = StringProperty('Bang!')
|
||||
message = StringProperty('')
|
||||
|
||||
|
||||
def install_exception_handler():
|
||||
ExceptionManager.add_handler(ErrorMessageOnException())
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
"""Turn the alert on Mi Band device
|
||||
"""
|
||||
from kivy.app import App
|
||||
from kivy.uix.button import Button
|
||||
|
||||
from able import BluetoothDispatcher, GATT_SUCCESS
|
||||
from error_message import install_exception_handler
|
||||
|
||||
|
||||
class BLE(BluetoothDispatcher):
|
||||
device = alert_characteristic = None
|
||||
|
||||
def start_alert(self, *args, **kwargs):
|
||||
if self.alert_characteristic: # alert service is already discovered
|
||||
self.alert(self.alert_characteristic)
|
||||
elif self.device: # device is already founded during the scan
|
||||
self.connect_gatt(self.device) # reconnect
|
||||
else:
|
||||
self.stop_scan() # stop previous scan
|
||||
self.start_scan() # start a scan for devices
|
||||
|
||||
def on_device(self, device, rssi, advertisement):
|
||||
# some device is found during the scan
|
||||
name = device.getName()
|
||||
if name and name.startswith('MI'): # is a Mi Band device
|
||||
self.device = device
|
||||
self.stop_scan()
|
||||
|
||||
def on_scan_completed(self):
|
||||
if self.device:
|
||||
self.connect_gatt(self.device) # connect to device
|
||||
|
||||
def on_connection_state_change(self, status, state):
|
||||
if status == GATT_SUCCESS and state: # connection established
|
||||
self.discover_services() # discover what services a device offer
|
||||
else: # disconnection or error
|
||||
self.alert_characteristic = None
|
||||
self.close_gatt() # close current connection
|
||||
|
||||
def on_services(self, status, services):
|
||||
# 0x2a06 is a standard code for "Alert Level" characteristic
|
||||
# https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.alert_level.xml
|
||||
self.alert_characteristic = services.search('2a06')
|
||||
self.alert(self.alert_characteristic)
|
||||
|
||||
def alert(self, characteristic):
|
||||
self.write_characteristic(characteristic, [2]) # 2 is for "High Alert"
|
||||
|
||||
|
||||
class AlertApp(App):
|
||||
|
||||
def build(self):
|
||||
self.ble = None
|
||||
return Button(text='Press to Alert Mi', on_press=self.start_alert)
|
||||
|
||||
def start_alert(self, *args, **kwargs):
|
||||
if not self.ble:
|
||||
self.ble = BLE()
|
||||
self.ble.start_alert()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
install_exception_handler()
|
||||
AlertApp().run()
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
"""Request MTU change, and write 100 bytes to a characteristic."""
|
||||
from kivy.app import App
|
||||
from kivy.clock import Clock
|
||||
from kivy.logger import Logger
|
||||
from kivy.uix.widget import Widget
|
||||
|
||||
from able import BluetoothDispatcher, GATT_SUCCESS
|
||||
|
||||
|
||||
class BLESender(BluetoothDispatcher):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.characteristic_to_write = None
|
||||
Clock.schedule_once(self.connect, 0)
|
||||
|
||||
def connect(self, _):
|
||||
self.connect_by_device_address("FF:FF:FF:FF:FF:FF")
|
||||
|
||||
def on_connection_state_change(self, status, state):
|
||||
if status == GATT_SUCCESS and state:
|
||||
self.discover_services()
|
||||
|
||||
def on_services(self, status, services):
|
||||
if status == GATT_SUCCESS:
|
||||
self.characteristic_to_write = services.search("0d03")
|
||||
# Need to request 100 + 3 extra bytes for ATT packet header
|
||||
self.request_mtu(103)
|
||||
|
||||
def on_mtu_changed(self, mtu, status):
|
||||
if status == GATT_SUCCESS and mtu == 103:
|
||||
Logger.info("MTU changed: now it is possible to send 100 bytes at once")
|
||||
self.write_characteristic(self.characteristic_to_write, range(100))
|
||||
else:
|
||||
Logger.error("MTU not changed: mtu=%d, status=%d", mtu, status)
|
||||
|
||||
def on_characteristic_write(self, characteristic, status):
|
||||
if status == GATT_SUCCESS:
|
||||
Logger.info("Characteristic write succeed")
|
||||
else:
|
||||
Logger.error("Write status: %d", status)
|
||||
|
||||
|
||||
class MTUApp(App):
|
||||
|
||||
def build(self):
|
||||
BLESender()
|
||||
return Widget()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
MTUApp().run()
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
[app]
|
||||
title = Multiple BLE devices
|
||||
version = 1.0
|
||||
package.name = multidevs
|
||||
package.domain = test.able
|
||||
source.dir = .
|
||||
source.include_exts = py,png,jpg,kv,atlas
|
||||
|
||||
requirements = python3,kivy,android,able_recipe
|
||||
|
||||
android.accept_sdk_license = True
|
||||
android.permissions =
|
||||
BLUETOOTH,
|
||||
BLUETOOTH_ADMIN,
|
||||
BLUETOOTH_SCAN,
|
||||
BLUETOOTH_CONNECT,
|
||||
BLUETOOTH_ADVERTISE,
|
||||
ACCESS_FINE_LOCATION
|
||||
|
||||
# android.api = 31
|
||||
# android.minapi = 31
|
||||
|
||||
|
||||
[buildozer]
|
||||
warn_on_root = 1
|
||||
# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
|
||||
log_level = 2
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
"""Scan for devices with name "KivyBLETest",
|
||||
connect and periodically read connected devices RSSI.
|
||||
|
||||
Multiple `BluetoothDispatcher` objects are used:
|
||||
one for the scanning process and one for every connected device.
|
||||
"""
|
||||
from able import GATT_SUCCESS, BluetoothDispatcher
|
||||
from able.filters import DeviceNameFilter
|
||||
from kivy.app import App
|
||||
from kivy.clock import Clock
|
||||
from kivy.logger import Logger
|
||||
from kivy.uix.label import Label
|
||||
|
||||
|
||||
class DeviceDispatcher(BluetoothDispatcher):
|
||||
"""Dispatcher to control a single BLE device."""
|
||||
|
||||
def __init__(self, device: "BluetoothDevice"):
|
||||
super().__init__()
|
||||
self._device = device
|
||||
self._address: str = device.getAddress()
|
||||
self._name: str = device.getName() or ""
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
return f"<{self._address}><{self._name}>"
|
||||
|
||||
def on_connection_state_change(self, status: int, state: int):
|
||||
if status == GATT_SUCCESS and state:
|
||||
Logger.info(f"Device: {self.title} connected")
|
||||
else:
|
||||
Logger.info(f"Device: {self.title} disconnected. {status=}, {state=}")
|
||||
self.close_gatt()
|
||||
Clock.schedule_once(callback=lambda dt: self.reconnect(), timeout=15)
|
||||
|
||||
def on_rssi_updated(self, rssi: int, status: int):
|
||||
Logger.info(f"Device: {self.title} RSSI: {rssi}")
|
||||
|
||||
def periodically_update_rssi(self):
|
||||
"""
|
||||
Clock callback to read
|
||||
the signal strength indicator for a connected device.
|
||||
"""
|
||||
if self.gatt: # if device is connected
|
||||
self.update_rssi()
|
||||
|
||||
def reconnect(self):
|
||||
Logger.info(f"Device: {self.title} try to reconnect ...")
|
||||
self.connect_gatt(self._device)
|
||||
|
||||
def start(self):
|
||||
"""Start connection to device."""
|
||||
if not self.gatt:
|
||||
self.connect_gatt(self._device)
|
||||
Clock.schedule_interval(
|
||||
callback=lambda dt: self.periodically_update_rssi(), timeout=5
|
||||
)
|
||||
|
||||
|
||||
class ScannerDispatcher(BluetoothDispatcher):
|
||||
"""Dispatcher to control the scanning process."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Stores connected devices addresses
|
||||
self._devices: dict[str, DeviceDispatcher] = {}
|
||||
|
||||
def on_scan_started(self, success: bool):
|
||||
if success:
|
||||
Logger.info("Scan: started")
|
||||
else:
|
||||
Logger.error("Scan: error on start")
|
||||
|
||||
def on_scan_completed(self):
|
||||
Logger.info("Scan: completed")
|
||||
|
||||
def on_device(self, device, rssi, advertisement):
|
||||
address = device.getAddress()
|
||||
if address not in self._devices:
|
||||
# Create dispatcher instance for a new device
|
||||
dispatcher = DeviceDispatcher(device)
|
||||
# Remember address,
|
||||
# to avoid multiple dispatchers creation for this device
|
||||
self._devices[address] = dispatcher
|
||||
Logger.info(f"Scan: device <{address}> added")
|
||||
dispatcher.start()
|
||||
|
||||
|
||||
class MultiDevicesApp(App):
|
||||
def build(self):
|
||||
ScannerDispatcher().start_scan(filters=[DeviceNameFilter("KivyBLETest")])
|
||||
return Label(text=self.name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
MultiDevicesApp().run()
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
[app]
|
||||
title = BLE advertising service
|
||||
version = 1.1
|
||||
package.name = advservice
|
||||
package.domain = test.able
|
||||
source.dir = .
|
||||
source.include_exts = py,png,jpg,kv,atlas
|
||||
|
||||
android.permissions =
|
||||
FOREGROUND_SERVICE,
|
||||
BLUETOOTH,
|
||||
BLUETOOTH_ADMIN,
|
||||
BLUETOOTH_CONNECT,
|
||||
BLUETOOTH_ADVERTISE
|
||||
|
||||
requirements = kivy==2.1.0,python3,able_recipe
|
||||
services = Able:service.py:foreground
|
||||
|
||||
android.accept_sdk_license = True
|
||||
|
||||
# android.api = 31
|
||||
# android.minapi = 31
|
||||
|
||||
[buildozer]
|
||||
warn_on_root = 1
|
||||
# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
|
||||
log_level = 2
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
"""Start advertising service."""
|
||||
from able import BluetoothDispatcher, Permission, require_bluetooth_enabled
|
||||
from jnius import autoclass
|
||||
from kivy.app import App
|
||||
from kivy.lang import Builder
|
||||
|
||||
|
||||
kv = """
|
||||
BoxLayout:
|
||||
Button:
|
||||
text: 'Start service'
|
||||
on_press: app.ble_dispatcher.start_service()
|
||||
Button:
|
||||
text: 'Stop service'
|
||||
on_press: app.ble_dispatcher.stop_service()
|
||||
"""
|
||||
|
||||
|
||||
class Dispatcher(BluetoothDispatcher):
|
||||
@property
|
||||
def service(self):
|
||||
return autoclass("test.able.advservice.ServiceAble")
|
||||
|
||||
@property
|
||||
def activity(self):
|
||||
return autoclass("org.kivy.android.PythonActivity").mActivity
|
||||
|
||||
# Need to turn on the adapter, before service is started
|
||||
@require_bluetooth_enabled
|
||||
def start_service(self):
|
||||
self.service.start(
|
||||
self.activity,
|
||||
# Pass UUID to advertise
|
||||
"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
||||
)
|
||||
App.get_running_app().stop() # Can close the app, service will continue running
|
||||
|
||||
def stop_service(self):
|
||||
self.service.stop(self.activity)
|
||||
|
||||
|
||||
class ServiceApp(App):
|
||||
def build(self):
|
||||
self.ble_dispatcher = Dispatcher(
|
||||
# This app does not use device scanning,
|
||||
# so the list of required permissions can be reduced
|
||||
runtime_permissions=[
|
||||
Permission.BLUETOOTH_CONNECT,
|
||||
Permission.BLUETOOTH_ADVERTISE,
|
||||
]
|
||||
)
|
||||
return Builder.load_string(kv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ServiceApp().run()
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
"""Service to advertise data, while not stopped."""
|
||||
import time
|
||||
from os import environ
|
||||
|
||||
from able import BluetoothDispatcher
|
||||
from able.advertising import (
|
||||
Advertiser,
|
||||
AdvertiseData,
|
||||
ServiceUUID,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
uuid = environ.get(
|
||||
"PYTHON_SERVICE_ARGUMENT",
|
||||
"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
)
|
||||
advertiser = Advertiser(
|
||||
ble=BluetoothDispatcher(),
|
||||
data=AdvertiseData(ServiceUUID(uuid)),
|
||||
)
|
||||
advertiser.start()
|
||||
while True:
|
||||
time.sleep(0xDEAD)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
[app]
|
||||
title = BLE scan dev service
|
||||
version = 1.1
|
||||
package.name = scanservice
|
||||
package.domain = test.able
|
||||
source.dir = .
|
||||
source.include_exts = py,png,jpg,kv,atlas
|
||||
|
||||
android.permissions =
|
||||
FOREGROUND_SERVICE,
|
||||
BLUETOOTH,
|
||||
BLUETOOTH_ADMIN,
|
||||
BLUETOOTH_SCAN,
|
||||
BLUETOOTH_CONNECT,
|
||||
BLUETOOTH_ADVERTISE,
|
||||
ACCESS_FINE_LOCATION
|
||||
|
||||
requirements = kivy==2.1.0,python3,able_recipe
|
||||
services = Able:service.py:foreground
|
||||
|
||||
android.accept_sdk_license = True
|
||||
|
||||
# android.api = 31
|
||||
# android.minapi = 31
|
||||
|
||||
[buildozer]
|
||||
warn_on_root = 1
|
||||
# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
|
||||
log_level = 2
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
"""Start BLE devices scaning service."""
|
||||
from able import (
|
||||
BluetoothDispatcher,
|
||||
require_bluetooth_enabled,
|
||||
)
|
||||
from jnius import autoclass
|
||||
from kivy.app import App
|
||||
from kivy.lang import Builder
|
||||
|
||||
|
||||
kv = """
|
||||
BoxLayout:
|
||||
Button:
|
||||
text: 'Start service'
|
||||
on_press: app.ble_dispatcher.start_service()
|
||||
Button:
|
||||
text: 'Stop service'
|
||||
on_press: app.ble_dispatcher.stop_service()
|
||||
"""
|
||||
|
||||
|
||||
class Dispatcher(BluetoothDispatcher):
|
||||
@property
|
||||
def service(self):
|
||||
return autoclass("test.able.scanservice.ServiceAble")
|
||||
|
||||
@property
|
||||
def activity(self):
|
||||
return autoclass("org.kivy.android.PythonActivity").mActivity
|
||||
|
||||
# Need to turn on the adapter and obtain permissions, before service is started
|
||||
@require_bluetooth_enabled
|
||||
def start_service(self):
|
||||
self.service.start(self.activity, "")
|
||||
App.get_running_app().stop() # Can close the app, service will continue to run
|
||||
|
||||
def stop_service(self):
|
||||
self.service.stop(self.activity)
|
||||
|
||||
|
||||
class ServiceApp(App):
|
||||
def build(self):
|
||||
self.ble_dispatcher = Dispatcher()
|
||||
return Builder.load_string(kv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ServiceApp().run()
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
"""Service to run BLE scan for 60 seconds,
|
||||
and log each `on_device` event.
|
||||
"""
|
||||
import time
|
||||
|
||||
from able import BluetoothDispatcher
|
||||
from kivy.logger import Logger
|
||||
|
||||
|
||||
class BLE(BluetoothDispatcher):
|
||||
def on_device(self, device, rssi, advertisement):
|
||||
title = device.getName() or device.getAddress()
|
||||
Logger.info("BLE Device found: %s", title)
|
||||
|
||||
def on_error(self, msg):
|
||||
Logger.error("BLE Error %s", msg)
|
||||
|
||||
|
||||
def main():
|
||||
ble = BLE()
|
||||
ble.start_scan()
|
||||
time.sleep(60)
|
||||
ble.stop_scan()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
"""
|
||||
Android Bluetooth Low Energy
|
||||
"""
|
||||
from pythonforandroid.recipe import PythonRecipe
|
||||
from pythonforandroid.toolchain import current_directory, info, shprint
|
||||
import sh
|
||||
from os.path import join
|
||||
|
||||
|
||||
class AbleRecipe(PythonRecipe):
|
||||
name = 'able_recipe'
|
||||
depends = ['python3', 'setuptools', 'android']
|
||||
call_hostpython_via_targetpython = False
|
||||
install_in_hostpython = True
|
||||
|
||||
def prepare_build_dir(self, arch):
|
||||
build_dir = self.get_build_dir(arch)
|
||||
assert build_dir.endswith(self.name)
|
||||
shprint(sh.rm, '-rf', build_dir)
|
||||
shprint(sh.mkdir, build_dir)
|
||||
|
||||
for filename in ('../../able', 'setup.py'):
|
||||
shprint(sh.cp, '-a', join(self.get_recipe_dir(), filename),
|
||||
build_dir)
|
||||
|
||||
def postbuild_arch(self, arch):
|
||||
super(AbleRecipe, self).postbuild_arch(arch)
|
||||
info('Copying able java class to classes build dir')
|
||||
with current_directory(self.get_build_dir(arch.arch)):
|
||||
shprint(sh.cp, '-a', join('able', 'src', 'org'),
|
||||
self.ctx.javaclass_dir)
|
||||
|
||||
|
||||
recipe = AbleRecipe()
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='able',
|
||||
version='0.0.0',
|
||||
packages=['able', 'able.android'],
|
||||
description='Bluetooth Low Energy for Android',
|
||||
license='MIT',
|
||||
)
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
import os
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from setuptools import setup
|
||||
from setuptools.command.install import install
|
||||
|
||||
|
||||
main_ns = {}
|
||||
with Path("able/version.py").open() as ver_file:
|
||||
exec(ver_file.read(), main_ns)
|
||||
|
||||
with Path("README.rst").open() as readme_file:
|
||||
long_description = readme_file.read()
|
||||
|
||||
|
||||
class PathParser:
|
||||
@property
|
||||
def javaclass_dir(self):
|
||||
path = self.build_dir / "javaclasses"
|
||||
if not path.exists():
|
||||
raise Exception(
|
||||
"Java classes directory is not found. "
|
||||
"Please report issue to: https://github.com/b3b/able/issues"
|
||||
)
|
||||
path = path / self.distribution_name
|
||||
print(f"Java classes directory found: '{path}'.")
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
@property
|
||||
def distribution_name(self):
|
||||
path = self.python_path
|
||||
while path.parent.name != "python-installs":
|
||||
if len(path.parts) <= 1:
|
||||
raise Exception(
|
||||
"Distribution name is not found. "
|
||||
"Please report issue to: https://github.com/b3b/able/issues"
|
||||
)
|
||||
path = path.parent
|
||||
print(f"Distribution name found: '{path.name}'.")
|
||||
return path.name
|
||||
|
||||
@property
|
||||
def build_dir(self):
|
||||
return self.python_installs_dir.parent
|
||||
|
||||
@property
|
||||
def python_installs_dir(self):
|
||||
path = self.python_path.parent
|
||||
while path.name != "python-installs":
|
||||
if len(path.parts) <= 1:
|
||||
raise Exception(
|
||||
"Python installs directory is not found. "
|
||||
"Please report issue to: https://github.com/b3b/able/issues"
|
||||
)
|
||||
path = path.parent
|
||||
return path
|
||||
|
||||
@property
|
||||
def python_path(self):
|
||||
cppflags = os.environ["CPPFLAGS"]
|
||||
print(f"Searching for Python install directory in CPPFLAGS: '{cppflags}'")
|
||||
match = re.search(r"-I(/[^\s]+/build/python-installs/[^/\s]+/)", cppflags)
|
||||
if not match:
|
||||
raise Exception("Can't find Python install directory.")
|
||||
found_path = Path(match.group(1))
|
||||
print("FOUND INSTALL DIRECTORY: "+found_path)
|
||||
return found_path
|
||||
|
||||
|
||||
class InstallRecipe(install):
|
||||
"""Command to install `able` recipe,
|
||||
copies Java files to distribution `javaclass` directory."""
|
||||
|
||||
def run(self):
|
||||
if False and "ANDROIDAPI" not in os.environ:
|
||||
raise Exception(
|
||||
"This recipe should not be installed directly, "
|
||||
"only with the buildozer tool."
|
||||
)
|
||||
|
||||
# Find Java classes target directory from the environment
|
||||
javaclass_dir = str(PathParser().javaclass_dir)
|
||||
|
||||
for java_file in (
|
||||
"able/src/org/able/BLE.java",
|
||||
"able/src/org/able/BLEAdvertiser.java",
|
||||
"able/src/org/able/PythonBluetooth.java",
|
||||
"able/src/org/able/PythonBluetoothAdvertiser.java",
|
||||
):
|
||||
shutil.copy(java_file, javaclass_dir)
|
||||
|
||||
install.run(self)
|
||||
|
||||
|
||||
setup(
|
||||
name="able_recipe",
|
||||
version=main_ns["__version__"],
|
||||
packages=["able", "able.android"],
|
||||
description="Bluetooth Low Energy for Android",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/x-rst",
|
||||
author="b3b",
|
||||
author_email="ash.b3b@gmail.com",
|
||||
install_requires=[],
|
||||
url="https://github.com/b3b/able",
|
||||
project_urls={
|
||||
"Changelog": "https://github.com/b3b/able/blob/master/CHANGELOG.rst",
|
||||
},
|
||||
# https://pypi.org/classifiers/
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: Android",
|
||||
"Topic :: System :: Networking",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
],
|
||||
keywords="android ble bluetooth kivy",
|
||||
license="MIT",
|
||||
zip_safe=False,
|
||||
cmdclass={
|
||||
"install": InstallRecipe,
|
||||
},
|
||||
options={
|
||||
"bdist_wheel": {
|
||||
# Changing the wheel name
|
||||
# to avoid installing a package from cache.
|
||||
"plat_name": "unused-nocache",
|
||||
},
|
||||
},
|
||||
)
|
||||
1
libs/able/testapps/bletest/.gitignore
vendored
1
libs/able/testapps/bletest/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
server
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
#:kivy 1.1.0
|
||||
#: import Factory kivy.factory.Factory
|
||||
#: import findall re.findall
|
||||
|
||||
<Caption@Label>:
|
||||
padding_left: '4sp'
|
||||
halign: 'left'
|
||||
text_size: self.size
|
||||
valign: 'middle'
|
||||
|
||||
<Value@Label>:
|
||||
padding_left: '4sp'
|
||||
halign: 'left'
|
||||
text_size: self.size
|
||||
valign: 'middle'
|
||||
|
||||
<ConnectByMACDialog@Popup>:
|
||||
title: 'Connect by MAC address'
|
||||
size_hint: None, None
|
||||
size: '400sp', '120sp'
|
||||
BoxLayout:
|
||||
orientation: 'vertical'
|
||||
pos: self.pos
|
||||
size: root.size
|
||||
|
||||
TextInput:
|
||||
size_hint_y: .5
|
||||
hint_text: 'Device address'
|
||||
input_filter: lambda value, _ : ''.join(findall('[0-9a-fA-F:]+', value)).upper()
|
||||
multiline: False
|
||||
text: app.device_address
|
||||
on_text: app.device_address = self.text
|
||||
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: .5
|
||||
Button:
|
||||
text: 'Connect'
|
||||
on_press: root.dismiss(), app.connect_by_mac_address()
|
||||
Button:
|
||||
text: 'Cancel'
|
||||
on_press: root.dismiss()
|
||||
|
||||
<MainLayout>:
|
||||
padding: '10sp'
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
GridLayout:
|
||||
cols: 2
|
||||
padding: '0sp'
|
||||
spacing: '0sp'
|
||||
orientation: 'lr-tb'
|
||||
|
||||
Caption:
|
||||
text: 'Adapter:'
|
||||
Value:
|
||||
text: app.adapter_state
|
||||
|
||||
Caption:
|
||||
text: 'State:'
|
||||
Value:
|
||||
text: app.state
|
||||
halign: 'left'
|
||||
valign: 'middle'
|
||||
text_size: self.size
|
||||
|
||||
Caption:
|
||||
text: 'Read test:'
|
||||
Value:
|
||||
text: app.test_string
|
||||
|
||||
Caption:
|
||||
text: 'Notifications count:'
|
||||
Value:
|
||||
text: app.notification_value
|
||||
|
||||
Caption:
|
||||
text: 'N packets sended:'
|
||||
Value:
|
||||
text: app.increment_count_value
|
||||
|
||||
Caption:
|
||||
text: 'N packets delivered:'
|
||||
Value:
|
||||
text: app.counter_value
|
||||
|
||||
Caption:
|
||||
text: 'Total transmission time:'
|
||||
Value:
|
||||
text: app.counter_total_time
|
||||
|
||||
BoxLayout:
|
||||
spacing: '20sp'
|
||||
orientation: 'vertical'
|
||||
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: .3
|
||||
|
||||
Button:
|
||||
text: 'Scan and connect'
|
||||
on_press: app.start_scan()
|
||||
|
||||
Button:
|
||||
text: 'Connect by MAC address'
|
||||
on_press: Factory.ConnectByMACDialog().open()
|
||||
|
||||
BoxLayout:
|
||||
id: queue_box
|
||||
orientation: 'vertical'
|
||||
size_hint_y: .15
|
||||
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
Caption:
|
||||
text: 'Enable GATT autoconnect:'
|
||||
CheckBox:
|
||||
id: timeout_checkbox
|
||||
active: app.autoconnect
|
||||
on_active: app.autoconnect = self.active
|
||||
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
size_hint_y: .2
|
||||
spacing: 10
|
||||
Button:
|
||||
disabled: app.state != 'connected'
|
||||
text: 'Read RSSI'
|
||||
on_press: app.read_rssi()
|
||||
Caption:
|
||||
text: 'RSSI Value:'
|
||||
Value:
|
||||
text: app.rssi
|
||||
|
||||
ToggleButton:
|
||||
disabled: app.state != 'connected'
|
||||
text: "Enable notifications"
|
||||
size_hint_y: .2
|
||||
on_state: app.enable_notifications(self.state == 'down')
|
||||
BoxLayout:
|
||||
id: queue_box
|
||||
orientation: 'vertical'
|
||||
disabled: app.state != 'connected'
|
||||
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
Caption:
|
||||
text: 'Enable BLE queue timeout:'
|
||||
CheckBox:
|
||||
id: timeout_checkbox
|
||||
active: app.queue_timeout_enabled
|
||||
on_active: app.queue_timeout_enabled = self.active
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
Caption:
|
||||
text: 'BLE queue timeout (ms):'
|
||||
TextInput:
|
||||
disabled: queue_box.disabled or not timeout_checkbox.active
|
||||
input_filter: 'int'
|
||||
multiline: False
|
||||
text: app.queue_timeout
|
||||
on_text: app.queue_timeout = self.text
|
||||
BoxLayout:
|
||||
Button:
|
||||
text: 'Apply queue settings'
|
||||
on_press: app.set_queue_settings()
|
||||
|
||||
BoxLayout:
|
||||
disabled: app.state != 'connected'
|
||||
orientation: 'vertical'
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
Caption:
|
||||
text: 'Transmission interval (ms):'
|
||||
TextInput:
|
||||
input_filter: 'int'
|
||||
multiline: False
|
||||
text: app.incremental_interval
|
||||
on_text: app.incremental_interval = self.text
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
Caption:
|
||||
text: 'Packet count limit:'
|
||||
TextInput:
|
||||
input_filter: 'int'
|
||||
multiline: False
|
||||
text: app.counter_max
|
||||
on_text: app.counter_max = self.text
|
||||
padding_bottom: '100sp'
|
||||
ToggleButton:
|
||||
width: self.texture_size[0] + 50
|
||||
text: "Enable transmission"
|
||||
on_state: app.enable_counter(self.state == 'down')
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
[app]
|
||||
title = BLE functions test
|
||||
version = 1.0
|
||||
package.name = kivy_ble_test
|
||||
package.domain = org.kivy
|
||||
source.dir = .
|
||||
source.include_exts = py,png,jpg,kv,atlas
|
||||
android.permissions = BLUETOOTH, BLUETOOTH_ADMIN, ACCESS_FINE_LOCATION
|
||||
requirements = python3,kivy,android,able_recipe
|
||||
|
||||
# (str) Android's logcat filters to use
|
||||
android.logcat_filters = *:S python:D
|
||||
|
||||
[buildozer]
|
||||
warn_on_root = 1
|
||||
# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
|
||||
log_level = 2
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
"""Connect to "KivyBLETest" server and test various BLE functions
|
||||
"""
|
||||
import time
|
||||
|
||||
from able import AdapterState, GATT_SUCCESS, BluetoothDispatcher
|
||||
from kivy.app import App
|
||||
from kivy.clock import Clock
|
||||
from kivy.config import Config
|
||||
from kivy.properties import BooleanProperty, StringProperty
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.storage.jsonstore import JsonStore
|
||||
|
||||
Config.set('kivy', 'log_level', 'debug')
|
||||
Config.set('kivy', 'log_enable', '1')
|
||||
|
||||
|
||||
class MainLayout(BoxLayout):
|
||||
pass
|
||||
|
||||
|
||||
class BLETestApp(App):
|
||||
ble = BluetoothDispatcher()
|
||||
adapter_state = StringProperty('')
|
||||
state = StringProperty('')
|
||||
test_string = StringProperty('')
|
||||
rssi = StringProperty('')
|
||||
notification_value = StringProperty('')
|
||||
counter_value = StringProperty('')
|
||||
increment_count_value = StringProperty('')
|
||||
incremental_interval = StringProperty('100')
|
||||
counter_max = StringProperty('128')
|
||||
counter_value = StringProperty('')
|
||||
counter_state = StringProperty('')
|
||||
counter_total_time = StringProperty('')
|
||||
queue_timeout_enabled = BooleanProperty(True)
|
||||
queue_timeout = StringProperty('1000')
|
||||
device_name = StringProperty('KivyBLETest')
|
||||
device_address = StringProperty('')
|
||||
autoconnect = BooleanProperty(False)
|
||||
|
||||
store = JsonStore('bletestapp.json')
|
||||
|
||||
uids = {
|
||||
'string': '0d01',
|
||||
'counter_reset': '0d02',
|
||||
'counter_increment': '0d03',
|
||||
'counter_read': '0d04',
|
||||
'notifications': '0d05'
|
||||
}
|
||||
|
||||
def build(self):
|
||||
if self.store.exists('device'):
|
||||
self.device_address = self.store.get('device')['address']
|
||||
else:
|
||||
self.device_address = ''
|
||||
return MainLayout()
|
||||
|
||||
def on_pause(self):
|
||||
return True
|
||||
|
||||
def on_resume(self):
|
||||
pass
|
||||
|
||||
def init(self):
|
||||
self.set_queue_settings()
|
||||
self.ble.bind(on_device=self.on_device)
|
||||
self.ble.bind(on_scan_started=self.on_scan_started)
|
||||
self.ble.bind(on_scan_completed=self.on_scan_completed)
|
||||
self.ble.bind(on_bluetooth_adapter_state_change=self.on_bluetooth_adapter_state_change)
|
||||
self.ble.bind(
|
||||
on_connection_state_change=self.on_connection_state_change)
|
||||
self.ble.bind(on_services=self.on_services)
|
||||
self.ble.bind(on_characteristic_read=self.on_characteristic_read)
|
||||
self.ble.bind(on_characteristic_changed=self.on_characteristic_changed)
|
||||
self.ble.bind(on_rssi_updated=self.on_rssi_updated)
|
||||
|
||||
def start_scan(self):
|
||||
if not self.state:
|
||||
self.init()
|
||||
self.state = 'scan_start'
|
||||
self.ble.close_gatt()
|
||||
self.ble.start_scan()
|
||||
|
||||
def connect_by_mac_address(self):
|
||||
self.store.put('device', address=self.device_address)
|
||||
if not self.state:
|
||||
self.init()
|
||||
self.state = 'try_connect'
|
||||
self.ble.close_gatt()
|
||||
try:
|
||||
self.ble.connect_by_device_address(
|
||||
self.device_address,
|
||||
autoconnect=self.autoconnect,
|
||||
)
|
||||
except ValueError as exc:
|
||||
self.state = str(exc)
|
||||
|
||||
def on_scan_started(self, ble, success):
|
||||
self.state = 'scan' if success else 'scan_error'
|
||||
|
||||
def on_device(self, ble, device, rssi, advertisement):
|
||||
if self.state != 'scan':
|
||||
return
|
||||
if device.getName() == self.device_name:
|
||||
self.device = device
|
||||
self.state = 'found'
|
||||
self.ble.stop_scan()
|
||||
|
||||
def on_scan_completed(self, ble):
|
||||
if self.device:
|
||||
self.ble.connect_gatt(
|
||||
self.device,
|
||||
autoconnect=self.autoconnect,
|
||||
)
|
||||
|
||||
def on_connection_state_change(self, ble, status, state):
|
||||
if status == GATT_SUCCESS:
|
||||
if state:
|
||||
self.ble.discover_services()
|
||||
else:
|
||||
self.state = 'disconnected'
|
||||
else:
|
||||
self.state = 'connection_error'
|
||||
|
||||
def on_services(self, ble, status, services):
|
||||
if status != GATT_SUCCESS:
|
||||
self.state = 'services_error'
|
||||
return
|
||||
self.state = 'connected'
|
||||
self.services = services
|
||||
self.read_test_string(ble)
|
||||
self.characteristics = {
|
||||
'counter_increment': self.services.search(
|
||||
self.uids['counter_increment']),
|
||||
'counter_reset': self.services.search(
|
||||
self.uids['counter_reset']),
|
||||
}
|
||||
|
||||
def on_bluetooth_adapter_state_change(self, ble, state):
|
||||
self.adapter_state = AdapterState(state).name
|
||||
|
||||
def read_rssi(self):
|
||||
self.rssi = '...'
|
||||
result = self.ble.update_rssi()
|
||||
|
||||
def on_rssi_updated(self, ble, rssi, status):
|
||||
self.rssi = str(rssi) if status == GATT_SUCCESS else f"Bad status: {status}"
|
||||
|
||||
def read_test_string(self, ble):
|
||||
characteristic = self.services.search(self.uids['string'])
|
||||
if characteristic:
|
||||
ble.read_characteristic(characteristic)
|
||||
else:
|
||||
self.test_string = 'not found'
|
||||
|
||||
def read_remote_counter(self):
|
||||
characteristic = self.services.search(self.uids['counter_read'])
|
||||
if characteristic:
|
||||
self.ble.read_characteristic(characteristic)
|
||||
else:
|
||||
self.counter_value = 'error'
|
||||
|
||||
def enable_notifications(self, enable):
|
||||
if enable:
|
||||
self.notification_value = '0'
|
||||
characteristic = self.services.search(self.uids['notifications'])
|
||||
if characteristic:
|
||||
self.ble.enable_notifications(characteristic, enable)
|
||||
else:
|
||||
self.notification_value = 'error'
|
||||
|
||||
def enable_counter(self, enable):
|
||||
if enable:
|
||||
self.counter_state = 'init'
|
||||
interval = int(self.incremental_interval) * .001
|
||||
Clock.schedule_interval(self.counter_next, interval)
|
||||
else:
|
||||
Clock.unschedule(self.counter_next)
|
||||
if self.counter_state != 'stop':
|
||||
self.counter_state = 'stop'
|
||||
self.read_remote_counter()
|
||||
|
||||
def counter_next(self, dt):
|
||||
if self.counter_state == 'init':
|
||||
self.counter_started_time = time.time()
|
||||
self.counter_total_time = ''
|
||||
self.reset_remote_counter()
|
||||
self.increment_remote_counter()
|
||||
elif self.counter_state == 'enabled':
|
||||
if int(self.increment_count_value) < int(self.counter_max):
|
||||
self.increment_remote_counter()
|
||||
else:
|
||||
self.enable_counter(False)
|
||||
|
||||
def reset_remote_counter(self):
|
||||
self.increment_count_value = '0'
|
||||
self.counter_value = ''
|
||||
self.ble.write_characteristic(self.characteristics['counter_reset'], [])
|
||||
self.counter_state = 'enabled'
|
||||
|
||||
def on_characteristic_read(self, ble, characteristic, status):
|
||||
uuid = characteristic.getUuid().toString()
|
||||
if self.uids['string'] in uuid:
|
||||
self.update_string_value(characteristic, status)
|
||||
elif self.uids['counter_read'] in uuid:
|
||||
self.counter_total_time = str(
|
||||
time.time() - self.counter_started_time)
|
||||
self.update_counter_value(characteristic, status)
|
||||
|
||||
def update_string_value(self, characteristic, status):
|
||||
result = 'ERROR'
|
||||
if status == GATT_SUCCESS:
|
||||
value = characteristic.getStringValue(0)
|
||||
if value == 'test':
|
||||
result = 'OK'
|
||||
self.test_string = result
|
||||
|
||||
def increment_remote_counter(self):
|
||||
characteristic = self.characteristics['counter_increment']
|
||||
self.ble.write_characteristic(characteristic, [])
|
||||
prev_value = int(self.increment_count_value)
|
||||
self.increment_count_value = str(prev_value + 1)
|
||||
|
||||
def update_counter_value(self, characteristic, status):
|
||||
if status == GATT_SUCCESS:
|
||||
self.counter_value = characteristic.getStringValue(0)
|
||||
else:
|
||||
self.counter_value = 'ERROR'
|
||||
|
||||
def set_queue_settings(self):
|
||||
self.ble.set_queue_timeout(None if not self.queue_timeout_enabled
|
||||
else int(self.queue_timeout) * .001)
|
||||
|
||||
def on_characteristic_changed(self, ble, characteristic):
|
||||
uuid = characteristic.getUuid().toString()
|
||||
if self.uids['notifications'] in uuid:
|
||||
prev_value = self.notification_value
|
||||
value = int(characteristic.getStringValue(0))
|
||||
if (prev_value == 'error') or (value != int(prev_value) + 1):
|
||||
value = 'error'
|
||||
self.notification_value = str(value)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
BLETestApp().run()
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
// +build
|
||||
|
||||
// based on https://github.com/paypal/gatt/blob/master/examples/server.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
"github.com/paypal/gatt"
|
||||
"github.com/paypal/gatt/linux/cmd"
|
||||
)
|
||||
|
||||
|
||||
var DefaultServerOptions = []gatt.Option{
|
||||
gatt.LnxMaxConnections(1),
|
||||
gatt.LnxDeviceID(-1, false),
|
||||
gatt.LnxSetAdvertisingParameters(&cmd.LESetAdvertisingParameters{
|
||||
AdvertisingIntervalMin: 0x04ff,
|
||||
AdvertisingIntervalMax: 0x04ff,
|
||||
AdvertisingChannelMap: 0x7,
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
func NewTestPythonService() *gatt.Service {
|
||||
n := 0
|
||||
s := gatt.NewService(gatt.MustParseUUID("16fe0d00-c111-11e3-b8c8-0002a5d5c51b"))
|
||||
|
||||
s.AddCharacteristic(gatt.MustParseUUID("16fe0d01-c111-11e3-b8c8-0002a5d5c51b")).HandleReadFunc(
|
||||
func(rsp gatt.ResponseWriter, req *gatt.ReadRequest) {
|
||||
n = 0
|
||||
log.Println("Echo")
|
||||
fmt.Fprintf(rsp, "test")
|
||||
})
|
||||
s.AddCharacteristic(gatt.MustParseUUID("16fe0d02-c111-11e3-b8c8-0002a5d5c51b")).HandleWriteFunc(
|
||||
func(r gatt.Request, data []byte) (status byte) {
|
||||
n = 0
|
||||
log.Println("Reset counter")
|
||||
return gatt.StatusSuccess
|
||||
})
|
||||
s.AddCharacteristic(gatt.MustParseUUID("16fe0d03-c111-11e3-b8c8-0002a5d5c51b")).HandleWriteFunc(
|
||||
func(r gatt.Request, data []byte) (status byte) {
|
||||
n++
|
||||
log.Println("Increment counter")
|
||||
return gatt.StatusSuccess
|
||||
})
|
||||
s.AddCharacteristic(gatt.MustParseUUID("16fe0d04-c111-11e3-b8c8-0002a5d5c51b")).HandleReadFunc(
|
||||
func(rsp gatt.ResponseWriter, req *gatt.ReadRequest) {
|
||||
log.Println("Response counter: ", n)
|
||||
fmt.Fprintf(rsp, "%d", n)
|
||||
})
|
||||
|
||||
s.AddCharacteristic(gatt.MustParseUUID("16fe0d05-c111-11e3-b8c8-0002a5d5c51b")).HandleNotifyFunc(
|
||||
func(r gatt.Request, n gatt.Notifier) {
|
||||
log.Println("Notifications enabled")
|
||||
cnt := 1
|
||||
for !n.Done() {
|
||||
fmt.Fprintf(n, "%d", cnt)
|
||||
cnt++
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
log.Println("Notifications disabled")
|
||||
})
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
d, err := gatt.NewDevice(DefaultServerOptions...)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open device, err: %s", err)
|
||||
}
|
||||
|
||||
d.Handle(
|
||||
gatt.CentralConnected(func(c gatt.Central) { fmt.Println("Connect: ", c.ID()) }),
|
||||
gatt.CentralDisconnected(func(c gatt.Central) { fmt.Println("Disconnect: ", c.ID()) }),
|
||||
)
|
||||
|
||||
onStateChanged := func(d gatt.Device, s gatt.State) {
|
||||
fmt.Printf("State: %s\n", s)
|
||||
switch s {
|
||||
case gatt.StatePoweredOn:
|
||||
s1 := NewTestPythonService()
|
||||
d.AddService(s1)
|
||||
d.AdvertiseNameAndServices("KivyBLETest", []gatt.UUID{s1.UUID()})
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
d.Init(onStateChanged)
|
||||
select {}
|
||||
}
|
||||
4
libs/able/tests/notebooks/.gitignore
vendored
4
libs/able/tests/notebooks/.gitignore
vendored
|
|
@ -1,4 +0,0 @@
|
|||
there.env
|
||||
.ipynb_checkpoints/
|
||||
*.asciidoc
|
||||
*.ipynb
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
---
|
||||
jupyter:
|
||||
jupytext:
|
||||
formats: ipynb,md
|
||||
text_representation:
|
||||
extension: .md
|
||||
format_name: markdown
|
||||
format_version: '1.3'
|
||||
jupytext_version: 1.11.2
|
||||
kernelspec:
|
||||
display_name: Python 3
|
||||
language: python
|
||||
name: python3
|
||||
---
|
||||
|
||||
```python
|
||||
from time import sleep
|
||||
%load_ext pythonhere
|
||||
%connect-there
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
from jnius import autoclass, cast
|
||||
|
||||
from able.android.dispatcher import (
|
||||
BluetoothDispatcher
|
||||
)
|
||||
|
||||
from able.scan_settings import (
|
||||
ScanSettings,
|
||||
ScanSettingsBuilder
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class Results:
|
||||
started: bool = None
|
||||
completed: bool = None
|
||||
devices: List = field(default_factory=lambda: [])
|
||||
```
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -ex
|
||||
|
||||
name="$1"
|
||||
command="${2}"
|
||||
command="${command:=test}"
|
||||
|
||||
|
||||
cat "${name}.md" |
|
||||
jupytext --execute --to ipynb |
|
||||
jupyter nbconvert --stdin --no-input --to asciidoc --output "${name}"
|
||||
|
||||
cat "${name}".asciidoc
|
||||
|
||||
if [ "${command}" = "test" ]; then
|
||||
diff "${name}.asciidoc" "${name}.expected"
|
||||
elif [ "${command}" = "record" ]; then
|
||||
cp "${name}".asciidoc "${name}".expected
|
||||
else
|
||||
echo "Unknown command: ${command}"
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
for name in test_*.md; do ./run "${name%%.*}"; done
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
[[setup]]
|
||||
= Setup
|
||||
|
||||
[[run-ble-devices-scan]]
|
||||
= Run BLE devices scan
|
||||
|
||||
|
||||
----
|
||||
Started: None Completed: None
|
||||
----
|
||||
|
||||
[[check-that-scan-started-and-completed]]
|
||||
= Check that scan started and completed
|
||||
|
||||
|
||||
----
|
||||
Started: 1 Completed: 1
|
||||
----
|
||||
|
||||
[[check-that-testing-device-was-discovered]]
|
||||
= Check that testing device was discovered
|
||||
|
||||
|
||||
----
|
||||
True
|
||||
----
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
---
|
||||
jupyter:
|
||||
jupytext:
|
||||
formats: ipynb,md
|
||||
text_representation:
|
||||
extension: .md
|
||||
format_name: markdown
|
||||
format_version: '1.3'
|
||||
jupytext_version: 1.11.2
|
||||
kernelspec:
|
||||
display_name: Python 3
|
||||
language: python
|
||||
name: python3
|
||||
---
|
||||
|
||||
# Setup
|
||||
|
||||
```python
|
||||
%run init.ipynb
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
class BLE(BluetoothDispatcher):
|
||||
|
||||
def on_scan_started(self, success):
|
||||
results.started = success
|
||||
|
||||
def on_scan_completed(self):
|
||||
results.completed = 1
|
||||
|
||||
def on_device(self, device, rssi, advertisement):
|
||||
results.devices.append(device)
|
||||
|
||||
ble = BLE()
|
||||
```
|
||||
|
||||
# Run BLE devices scan
|
||||
|
||||
```python
|
||||
%%there
|
||||
results = Results()
|
||||
print(f"Started: {results.started} Completed: {results.completed}")
|
||||
ble.start_scan()
|
||||
```
|
||||
|
||||
```python
|
||||
sleep(10)
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
ble.stop_scan()
|
||||
```
|
||||
|
||||
```python
|
||||
sleep(2)
|
||||
```
|
||||
|
||||
# Check that scan started and completed
|
||||
|
||||
```python
|
||||
%%there
|
||||
print(f"Started: {results.started} Completed: {results.completed}")
|
||||
```
|
||||
|
||||
# Check that testing device was discovered
|
||||
|
||||
```python
|
||||
%%there
|
||||
print(
|
||||
"KivyBLETest" in [dev.getName() for dev in results.devices]
|
||||
)
|
||||
```
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
[[setup]]
|
||||
= Setup
|
||||
|
||||
[[test-device-is-found-with-scan-filters-set]]
|
||||
= Test device is found with scan filters set
|
||||
|
||||
|
||||
----
|
||||
{'KivyBLETest'}
|
||||
----
|
||||
|
||||
[[test-device-is-not-found-filtered-out-by-name]]
|
||||
= Test device is not found: filtered out by name
|
||||
|
||||
|
||||
----
|
||||
[]
|
||||
----
|
||||
|
||||
[[test-scan-filter-mathes]]
|
||||
= Test scan filter mathes
|
||||
|
||||
|
||||
----
|
||||
EmptyFilter() True
|
||||
EmptyFilter() True
|
||||
EmptyFilter() True
|
||||
----
|
||||
|
||||
|
||||
----
|
||||
DeviceAddressFilter(address='AA:AA:AA:AA:AA:AA') True
|
||||
DeviceAddressFilter(address='AA:AA:AA:AA:AA:AB') False
|
||||
AA is not a valid Bluetooth address
|
||||
----
|
||||
|
||||
|
||||
----
|
||||
DeviceNameFilter(name='KivyBLETest') True
|
||||
DeviceNameFilter(name='KivyBLETes') False
|
||||
----
|
||||
|
||||
|
||||
----
|
||||
ManufacturerDataFilter(id=76, data=[], mask=None) False
|
||||
ManufacturerDataFilter(id=76, data=[], mask=None) True
|
||||
ManufacturerDataFilter(id=76, data=[2, 21, 141, 166, 131, 214, 229], mask=None) True
|
||||
ManufacturerDataFilter(id=76, data=[2, 21, 141, 166, 131, 214, 170], mask=None) False
|
||||
ManufacturerDataFilter(id=76, data=[2, 21, 141, 166, 131, 214, 170], mask=[255, 255, 255, 255, 255, 255, 0]) True
|
||||
ManufacturerDataFilter(id=76, data=[2, 21, 141, 166, 131, 214, 170], mask=[255, 255, 255, 255, 255, 255, 255]) False
|
||||
ManufacturerDataFilter(id=76, data=[2, 0, 141, 166, 131], mask=[255, 0, 255, 255, 255]) True
|
||||
ManufacturerDataFilter(id=76, data=b'\x02\x15', mask=None) True
|
||||
ManufacturerDataFilter(id=76, data=b'\x02\x16', mask=None) False
|
||||
----
|
||||
|
||||
|
||||
----
|
||||
ServiceDataFilter(uid='0000180f-0000-1000-8000-00805f9b34fb', data=[], mask=None) True
|
||||
ServiceDataFilter(uid='0000180f-0000-1000-8000-00805f9b34fc', data=[], mask=None) False
|
||||
ServiceDataFilter(uid='0000180f-0000-1000-8000-00805f9b34fb', data=[34], mask=None) True
|
||||
ServiceDataFilter(uid='0000180f-0000-1000-8000-00805f9b34fb', data=[33], mask=None) False
|
||||
ServiceDataFilter(uid='0000180f-0000-1000-8000-00805f9b34fb', data=[33], mask=[240]) True
|
||||
ServiceDataFilter(uid='0000180f-0000-1000-8000-00805f9b34fb', data=[33], mask=[15]) False
|
||||
----
|
||||
|
||||
|
||||
----
|
||||
ServiceUUIDFilter(uid='16fe0d00-c111-11e3-b8c8-0002a5d5c51B', mask=None) True
|
||||
ServiceUUIDFilter(uid='16fe0d00-c111-11e3-b8c8-0002a5d5c51C', mask=None) False
|
||||
ServiceUUIDFilter(uid='16fe0d00-c111-11e3-b8c8-0002a5d5c51C', mask='ffffffff-ffff-ffff-ffff-ffffffffffff') False
|
||||
ServiceUUIDFilter(uid='16fe0d00-c111-11e3-b8c8-0002a5d5c51C', mask='ffffffff-ffff-ffff-ffff-fffffffffff0') True
|
||||
----
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
---
|
||||
jupyter:
|
||||
jupytext:
|
||||
formats: ipynb,md
|
||||
text_representation:
|
||||
extension: .md
|
||||
format_name: markdown
|
||||
format_version: '1.3'
|
||||
jupytext_version: 1.11.2
|
||||
kernelspec:
|
||||
display_name: Python 3
|
||||
language: python
|
||||
name: python3
|
||||
---
|
||||
|
||||
# Setup
|
||||
|
||||
```python
|
||||
%run init.ipynb
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
from able.filters import *
|
||||
|
||||
class BLE(BluetoothDispatcher):
|
||||
|
||||
def on_scan_started(self, success):
|
||||
results.started = success
|
||||
|
||||
def on_scan_completed(self):
|
||||
results.completed = 1
|
||||
|
||||
def on_device(self, device, rssi, advertisement):
|
||||
results.devices.append(device)
|
||||
|
||||
ble = BLE()
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
BluetoothDevice = autoclass("android.bluetooth.BluetoothDevice")
|
||||
ScanResult = autoclass("android.bluetooth.le.ScanResult")
|
||||
ScanRecord = autoclass("android.bluetooth.le.ScanRecord")
|
||||
Parcel = autoclass("android/os/Parcel")
|
||||
ParcelUuid = autoclass('android.os.ParcelUuid')
|
||||
|
||||
def filter_matches(f, scan_result):
|
||||
print(f, f.build().matches(scan_result))
|
||||
|
||||
def mock_device():
|
||||
"""Return BluetoothDevice instance with address=AA:AA:AA:AA:AA:AA"""
|
||||
device_data = [17, 0, 0, 0, 65, 0, 65, 0, 58, 0, 65, 0, 65, 0, 58, 0, 65, 0, 65, 0, 58, 0, 65, 0, 65, 0, 58, 0, 65, 0, 65, 0, 58, 0, 65, 0, 65]
|
||||
p = Parcel.obtain()
|
||||
p.unmarshall(device_data, 0, len(device_data))
|
||||
p.setDataPosition(0)
|
||||
return BluetoothDevice.CREATOR.createFromParcel(p)
|
||||
|
||||
|
||||
def mock_scan_result(record):
|
||||
return ScanResult(mock_device(), ScanRecord.parseFromBytes(record), -33, 1633954394000)
|
||||
|
||||
def mock_test_app_scan_result():
|
||||
return mock_scan_result(
|
||||
[2, 1, 6, 17, 6, 27, 197, 213, 165, 2, 0, 200, 184, 227, 17, 17, 193, 0, 13,
|
||||
254, 22, 12, 9, 75, 105, 118, 121, 66, 76, 69, 84, 101, 115, 116,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
]
|
||||
)
|
||||
|
||||
def mock_beacon_scan_result():
|
||||
"""0x4C, # Apple Manufacturer ID
|
||||
bytes([
|
||||
0x2, # SubType: Custom Manufacturer Data
|
||||
0x15 # Subtype lenth
|
||||
]) +
|
||||
uuid + # UUID of beacon: UUID=8da683d6-e574-4a2e-bb9b-d83f2d05fc12
|
||||
bytes([
|
||||
0, 15, # Major value
|
||||
0, 1, # Minor value
|
||||
10 # RSSI, dBm at 1m
|
||||
])
|
||||
"""
|
||||
return mock_scan_result(bytes.fromhex('1AFF4C0002158DA683D6E5744A2EBB9BD83F2D05FC12000F00010A'))
|
||||
|
||||
def mock_battery_scan_result():
|
||||
"""Battery ("0000180f-0000-1000-8000-00805f9b34fb" or "180f" in short form)
|
||||
service data: 34% (0x22)
|
||||
"""
|
||||
return mock_scan_result(bytes.fromhex('04160F1822'))
|
||||
|
||||
beacon = mock_beacon_scan_result()
|
||||
battery = mock_battery_scan_result()
|
||||
testapp = mock_test_app_scan_result()
|
||||
```
|
||||
|
||||
# Test device is found with scan filters set
|
||||
|
||||
```python
|
||||
%%there
|
||||
results = Results()
|
||||
ble.start_scan(filters=[
|
||||
DeviceNameFilter("KivyBLETest") & ServiceUUIDFilter("16fe0d00-c111-11e3-b8c8-0002a5d5c51b"),
|
||||
])
|
||||
```
|
||||
|
||||
```python
|
||||
sleep(10)
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
ble.stop_scan()
|
||||
```
|
||||
|
||||
```python
|
||||
sleep(2)
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
print(set([dev.getName() for dev in results.devices]))
|
||||
```
|
||||
|
||||
# Test device is not found: filtered out by name
|
||||
|
||||
```python
|
||||
%%there
|
||||
results = Results()
|
||||
ble.start_scan(filters=[DeviceNameFilter("No-such-device-8458e2e35158")])
|
||||
```
|
||||
|
||||
```python
|
||||
sleep(10)
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
ble.stop_scan()
|
||||
```
|
||||
|
||||
```python
|
||||
sleep(2)
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
print(results.devices)
|
||||
```
|
||||
|
||||
# Test scan filter mathes
|
||||
|
||||
```python
|
||||
%%there
|
||||
filter_matches(EmptyFilter(), testapp)
|
||||
filter_matches(EmptyFilter(), beacon)
|
||||
filter_matches(EmptyFilter(), battery)
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
filter_matches(DeviceAddressFilter("AA:AA:AA:AA:AA:AA"), testapp)
|
||||
filter_matches(DeviceAddressFilter("AA:AA:AA:AA:AA:AB"), testapp)
|
||||
try:
|
||||
filter_matches(DeviceAddressFilter("AA"), testapp)
|
||||
except Exception as exc:
|
||||
print(exc)
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
filter_matches(DeviceNameFilter("KivyBLETest"), testapp)
|
||||
filter_matches(DeviceNameFilter("KivyBLETes"), testapp)
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
filter_matches(ManufacturerDataFilter(0x4c, []), testapp)
|
||||
filter_matches(ManufacturerDataFilter(0x4c, []), beacon)
|
||||
filter_matches(ManufacturerDataFilter(0x4c, [0x2, 0x15, 0x8d, 0xa6, 0x83, 0xd6, 0xe5]), beacon)
|
||||
filter_matches(ManufacturerDataFilter(0x4c, [0x2, 0x15, 0x8d, 0xa6, 0x83, 0xd6, 0xaa]), beacon)
|
||||
filter_matches(
|
||||
ManufacturerDataFilter(0x4c,
|
||||
[0x2, 0x15, 0x8d, 0xa6, 0x83, 0xd6, 0xaa],
|
||||
[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00]),
|
||||
beacon
|
||||
)
|
||||
filter_matches(
|
||||
ManufacturerDataFilter(0x4c,
|
||||
[0x2, 0x15, 0x8d, 0xa6, 0x83, 0xd6, 0xaa],
|
||||
[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]),
|
||||
beacon
|
||||
)
|
||||
filter_matches(ManufacturerDataFilter(0x4c, [0x2, 0, 0x8d, 0xa6, 0x83], [0xff, 0, 0xff, 0xff, 0xff]), beacon)
|
||||
filter_matches(ManufacturerDataFilter(0x4c, b'\x02\x15'), beacon)
|
||||
filter_matches(ManufacturerDataFilter(0x4c, b'\x02\x16'), beacon)
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
filter_matches(ServiceDataFilter("0000180f-0000-1000-8000-00805f9b34fb", []), battery)
|
||||
filter_matches(ServiceDataFilter("0000180f-0000-1000-8000-00805f9b34fc", []), battery)
|
||||
|
||||
filter_matches(ServiceDataFilter("0000180f-0000-1000-8000-00805f9b34fb", [0x22]), battery)
|
||||
filter_matches(ServiceDataFilter("0000180f-0000-1000-8000-00805f9b34fb", [0x21]), battery)
|
||||
filter_matches(ServiceDataFilter("0000180f-0000-1000-8000-00805f9b34fb", [0x21], mask=[0xf0]), battery)
|
||||
filter_matches(ServiceDataFilter("0000180f-0000-1000-8000-00805f9b34fb", [0x21], mask=[0x0f]), battery)
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
filter_matches(ServiceUUIDFilter("16fe0d00-c111-11e3-b8c8-0002a5d5c51B"), testapp)
|
||||
filter_matches(ServiceUUIDFilter("16fe0d00-c111-11e3-b8c8-0002a5d5c51C"), testapp)
|
||||
filter_matches(ServiceUUIDFilter(
|
||||
"16fe0d00-c111-11e3-b8c8-0002a5d5c51C",
|
||||
"ffffffff-ffff-ffff-ffff-ffffffffffff"
|
||||
), testapp)
|
||||
filter_matches(ServiceUUIDFilter(
|
||||
"16fe0d00-c111-11e3-b8c8-0002a5d5c51C",
|
||||
"ffffffff-ffff-ffff-ffff-fffffffffff0"
|
||||
), testapp)
|
||||
```
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
[[setup]]
|
||||
= Setup
|
||||
|
||||
[[run-scan_mode_low_power]]
|
||||
= Run SCAN_MODE_LOW_POWER
|
||||
|
||||
|
||||
----
|
||||
True
|
||||
----
|
||||
|
||||
[[run-scan_mode_low_latency]]
|
||||
= Run SCAN_MODE_LOW_LATENCY
|
||||
|
||||
|
||||
----
|
||||
True
|
||||
----
|
||||
|
||||
[[check-that-received-advertisement-count-is-greater-with-scan_mode_low_latency]]
|
||||
= Check that received advertisement count is greater with
|
||||
SCAN_MODE_LOW_LATENCY
|
||||
|
||||
|
||||
----
|
||||
True
|
||||
----
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
---
|
||||
jupyter:
|
||||
jupytext:
|
||||
formats: ipynb,md
|
||||
text_representation:
|
||||
extension: .md
|
||||
format_name: markdown
|
||||
format_version: '1.3'
|
||||
jupytext_version: 1.11.2
|
||||
kernelspec:
|
||||
display_name: Python 3
|
||||
language: python
|
||||
name: python3
|
||||
---
|
||||
|
||||
# Setup
|
||||
|
||||
```python
|
||||
%run init.ipynb
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
class BLE(BluetoothDispatcher):
|
||||
|
||||
def on_scan_started(self, success):
|
||||
results.started = success
|
||||
|
||||
def on_scan_completed(self):
|
||||
results.completed = 1
|
||||
|
||||
def on_device(self, device, rssi, advertisement):
|
||||
results.devices.append(device)
|
||||
|
||||
def get_advertisemnt_count():
|
||||
return len([dev for dev in results.devices if dev.getName() == "KivyBLETest"])
|
||||
|
||||
ble = BLE()
|
||||
```
|
||||
|
||||
# Run SCAN_MODE_LOW_POWER
|
||||
|
||||
```python
|
||||
%%there
|
||||
results = Results()
|
||||
ble.start_scan(
|
||||
settings=ScanSettingsBuilder().setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
|
||||
)
|
||||
```
|
||||
|
||||
```python
|
||||
sleep(20)
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
ble.stop_scan()
|
||||
```
|
||||
|
||||
```python
|
||||
sleep(2)
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
low_power_advertisement_count = get_advertisemnt_count()
|
||||
print(low_power_advertisement_count > 0)
|
||||
```
|
||||
|
||||
# Run SCAN_MODE_LOW_LATENCY
|
||||
|
||||
```python
|
||||
%%there
|
||||
results = Results()
|
||||
|
||||
ble.start_scan(
|
||||
settings=ScanSettingsBuilder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
||||
)
|
||||
```
|
||||
|
||||
```python
|
||||
sleep(20)
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
ble.stop_scan()
|
||||
```
|
||||
|
||||
```python
|
||||
sleep(2)
|
||||
```
|
||||
|
||||
```python
|
||||
%%there
|
||||
low_latency_advertisement_count = get_advertisemnt_count()
|
||||
print(low_latency_advertisement_count > 0)
|
||||
```
|
||||
|
||||
# Check that received advertisement count is greater with SCAN_MODE_LOW_LATENCY
|
||||
|
||||
```python
|
||||
%%there
|
||||
print(low_latency_advertisement_count - low_power_advertisement_count > 2)
|
||||
```
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
import pytest
|
||||
|
||||
from able.adapter import (
|
||||
AdapterManager,
|
||||
require_bluetooth_enabled,
|
||||
set_adapter_failure_rollback,
|
||||
)
|
||||
from able.android.dispatcher import BluetoothDispatcher
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def manager(mocker):
|
||||
return AdapterManager(mocker.Mock(), ..., [])
|
||||
|
||||
|
||||
def test_operation_executed(mocker, manager):
|
||||
operation = mocker.Mock()
|
||||
logger = mocker.patch("able.adapter.Logger")
|
||||
|
||||
manager.execute(operation)
|
||||
|
||||
operation.assert_called_once()
|
||||
logger.exception.assert_not_called()
|
||||
|
||||
|
||||
def test_operation_failed_as_expected(mocker, manager):
|
||||
manager.check_permissions = mocker.Mock(return_value=False)
|
||||
expected = Exception("expected")
|
||||
operation = mocker.Mock(side_effect=expected)
|
||||
logger = mocker.patch("able.adapter.Logger")
|
||||
|
||||
manager.execute(operation)
|
||||
operation.assert_not_called()
|
||||
|
||||
manager.check_permissions = mocker.Mock(return_value=True)
|
||||
manager.execute_operations()
|
||||
|
||||
operation.assert_called_once()
|
||||
logger.exception.assert_called_once_with(expected)
|
||||
|
||||
|
||||
def test_operations_executed(mocker, manager):
|
||||
operations = [mocker.Mock(), mocker.Mock()]
|
||||
manager.operations = operations.copy()
|
||||
manager.check_permissions = mocker.Mock(return_value=False)
|
||||
|
||||
manager.execute_operations()
|
||||
|
||||
# permissions not granted = > suspended
|
||||
calls = [operation.call_count for operation in manager.operations]
|
||||
assert calls == [0, 0]
|
||||
assert manager.operations == operations
|
||||
|
||||
# one more operation requested
|
||||
manager.execute(next_operation := mocker.Mock())
|
||||
|
||||
assert [operation.call_count for operation in manager.operations] == [0, 0, 0]
|
||||
assert manager.operations == operations + [next_operation]
|
||||
|
||||
manager.check_permissions = mocker.Mock(return_value=True)
|
||||
manager.execute_operations()
|
||||
assert not manager.operations
|
||||
assert [operation.call_count for operation in operations + [next_operation]] == [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
]
|
||||
|
||||
|
||||
def test_rollback_performed(mocker, manager):
|
||||
handlers = [mocker.Mock(), mocker.Mock()]
|
||||
operations = [mocker.Mock(), mocker.Mock()]
|
||||
|
||||
manager.operations = operations.copy()
|
||||
manager.rollback_handlers = handlers.copy()
|
||||
manager.rollback()
|
||||
|
||||
assert not manager.rollback_handlers
|
||||
assert not manager.operations
|
||||
assert [operation.call_count for operation in operations] == [0, 0]
|
||||
assert [operation.call_count for operation in handlers] == [1, 1]
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import unittest
|
||||
import mock
|
||||
from able.queue import ble_task
|
||||
|
||||
|
||||
class TestBLETask(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.queue = mock.Mock()
|
||||
self.task_called = None
|
||||
|
||||
@ble_task
|
||||
def increment(self, a=1, b=0):
|
||||
self.task_called = a + b
|
||||
|
||||
def test_method_not_executed(self):
|
||||
self.increment()
|
||||
self.assertEqual(self.task_called, None)
|
||||
|
||||
def test_task_enqued(self):
|
||||
self.increment()
|
||||
self.assertTrue(self.queue.enque.called)
|
||||
|
||||
def test_task_default_arguments(self):
|
||||
self.increment()
|
||||
task = self.queue.enque.call_args[0][0]
|
||||
task()
|
||||
self.assertEqual(self.task_called, 1)
|
||||
|
||||
def test_task_arguments_passed(self):
|
||||
self.increment(200, 11)
|
||||
task = self.queue.enque.call_args[0][0]
|
||||
task()
|
||||
self.assertEqual(self.task_called, 211)
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import pytest
|
||||
|
||||
from able.android.dispatcher import BluetoothDispatcher
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ble(mocker):
|
||||
mocker.patch("able.android.dispatcher.PythonBluetooth")
|
||||
ble = BluetoothDispatcher()
|
||||
ble._ble = mocker.Mock()
|
||||
ble.on_scan_started = mocker.Mock()
|
||||
return ble
|
||||
|
||||
|
||||
def test_adapter_returned(mocker, ble):
|
||||
manager = ble._adapter_manager
|
||||
manager.check_permissions = mocker.Mock(return_value=False)
|
||||
assert not ble.adapter
|
||||
assert not ble.adapter
|
||||
|
||||
manager.check_permissions = mocker.Mock(return_value=True)
|
||||
assert ble.adapter
|
||||
|
||||
|
||||
def test_start_scan_executed(ble):
|
||||
manager = ble._adapter_manager
|
||||
assert manager
|
||||
|
||||
ble.start_scan()
|
||||
ble._ble.startScan.assert_called_once()
|
||||
|
||||
|
||||
def test_start_scan_failed_as_expected(mocker, ble):
|
||||
manager = ble._adapter_manager
|
||||
manager.check_permissions = mocker.Mock(return_value=False)
|
||||
|
||||
ble.start_scan()
|
||||
ble._ble.startScan.assert_not_called()
|
||||
|
||||
assert len(manager.operations) == 1
|
||||
assert len(manager.rollback_handlers) == 1
|
||||
|
||||
manager.on_runtime_permissions(permissions=[...], grant_results=[False])
|
||||
|
||||
ble.on_scan_started.assert_called_once_with(success=False)
|
||||
assert len(manager.operations) == 0
|
||||
assert len(manager.rollback_handlers) == 0
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import pytest
|
||||
|
||||
import able.filters as filters
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def java_builder(mocker):
|
||||
instance = mocker.Mock()
|
||||
mocker.patch("able.filters.ScanFilterBuilder", return_value=instance)
|
||||
return instance
|
||||
|
||||
|
||||
def test_filter_builded(java_builder):
|
||||
filters.Filter().build()
|
||||
assert java_builder.build.call_count == 1
|
||||
|
||||
|
||||
def test_builder_method_called(java_builder):
|
||||
f = filters.DeviceNameFilter("test")
|
||||
|
||||
f.build()
|
||||
|
||||
assert java_builder.method_calls == [
|
||||
("setDeviceName", ("test",)),
|
||||
("build", )
|
||||
]
|
||||
|
||||
|
||||
def test_filters_combined(java_builder):
|
||||
f = filters.DeviceNameFilter("test") & (
|
||||
filters.DeviceAddressFilter("AA:AA:AA:AA:AA:AA") &
|
||||
filters.ManufacturerDataFilter("test-id", [1, 2, 3])
|
||||
)
|
||||
|
||||
f.build()
|
||||
|
||||
assert java_builder.method_calls == [
|
||||
("setDeviceName", ("test",)),
|
||||
("setDeviceAddress", ("AA:AA:AA:AA:AA:AA",)),
|
||||
("setManufacturerData", ("test-id", [1, 2, 3])),
|
||||
("build", )
|
||||
]
|
||||
|
||||
|
||||
def test_combine_same_type_exception(java_builder):
|
||||
with pytest.raises(ValueError, match="cannot combine filters of the same type"):
|
||||
f = filters.DeviceNameFilter("test") & (
|
||||
filters.DeviceAddressFilter("AA:AA:AA:AA:AA:AA") &
|
||||
filters.DeviceNameFilter("test2")
|
||||
)
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def parser(mocker):
|
||||
mocker.patch("setuptools.setup")
|
||||
from setup import PathParser
|
||||
|
||||
return PathParser()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("cppflags", "expected"),
|
||||
[
|
||||
(
|
||||
"-I/media/able/examples/alert/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/python-installs/alert_mi/",
|
||||
"/media/able/examples/alert/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/javaclasses/alert_mi",
|
||||
),
|
||||
(
|
||||
"-DANDROID -I/home/user/.buildozer/android/platform/android-ndk-r25b/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include -I/media/able/examples/alert/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/python-installs/alert_mi/arm64-v8a/include/python3.9",
|
||||
"/media/able/examples/alert/.buildozer/android/platform/build-arm64-v8a_armeabi-v7a/build/javaclasses/alert_mi",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_javaclass_dir_found(mocker, parser, cppflags, expected):
|
||||
mocker.patch("os.environ", {"CPPFLAGS": cppflags})
|
||||
mocker.patch("pathlib.Path.exists", return_value=True)
|
||||
mocker.patch("pathlib.Path.mkdir")
|
||||
assert parser.javaclass_dir == Path(expected)
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
"""
|
||||
Android Bluetooth Low Energy
|
||||
"""
|
||||
from pythonforandroid.recipe import PythonRecipe
|
||||
from pythonforandroid.toolchain import current_directory, info, shprint
|
||||
import sh
|
||||
from os.path import join
|
||||
|
||||
|
||||
class AbleRecipe(PythonRecipe):
|
||||
name = 'able_recipe'
|
||||
depends = ['python3', 'setuptools', 'android']
|
||||
call_hostpython_via_targetpython = False
|
||||
install_in_hostpython = True
|
||||
|
||||
def prepare_build_dir(self, arch):
|
||||
build_dir = self.get_build_dir(arch)
|
||||
assert build_dir.endswith(self.name)
|
||||
shprint(sh.rm, '-rf', build_dir)
|
||||
shprint(sh.mkdir, build_dir)
|
||||
|
||||
srcs = ('../../libs/able/able', 'setup.py')
|
||||
|
||||
for filename in srcs:
|
||||
print(f"Copy {join(self.get_recipe_dir(), filename)} to {build_dir}")
|
||||
shprint(sh.cp, '-a', join(self.get_recipe_dir(), filename),
|
||||
build_dir)
|
||||
|
||||
def postbuild_arch(self, arch):
|
||||
super(AbleRecipe, self).postbuild_arch(arch)
|
||||
info('Copying able java class to classes build dir')
|
||||
with current_directory(self.get_build_dir(arch.arch)):
|
||||
shprint(sh.cp, '-a', join('able', 'src', 'org'),
|
||||
self.ctx.javaclass_dir)
|
||||
|
||||
|
||||
recipe = AbleRecipe()
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='able',
|
||||
version='0.0.0',
|
||||
packages=['able', 'able.android'],
|
||||
description='Bluetooth Low Energy for Android',
|
||||
license='MIT',
|
||||
)
|
||||
|
|
@ -2,7 +2,6 @@ from os.path import join
|
|||
from pythonforandroid.recipe import Recipe
|
||||
from pythonforandroid.toolchain import current_directory, shprint
|
||||
import sh
|
||||
import os
|
||||
|
||||
# For debugging, clean with
|
||||
# buildozer android p4a -- clean_recipe_build codec2 --local-recipes ~/Information/Source/Sideband/recipes
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
from pythonforandroid.recipe import CompiledComponentsPythonRecipe
|
||||
|
||||
|
||||
class CythonRecipe(CompiledComponentsPythonRecipe):
|
||||
|
||||
version = '3.1.6'
|
||||
url = 'https://github.com/cython/cython/archive/{version}.tar.gz'
|
||||
site_packages_name = 'cython'
|
||||
depends = ['setuptools']
|
||||
call_hostpython_via_targetpython = False
|
||||
install_in_hostpython = True
|
||||
|
||||
|
||||
recipe = CythonRecipe()
|
||||
1516
recipes/ffpyplayer/__init__.py
Normal file
1516
recipes/ffpyplayer/__init__.py
Normal file
File diff suppressed because it is too large
Load diff
15
recipes/ffpyplayer/setup.py.patch
Normal file
15
recipes/ffpyplayer/setup.py.patch
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
--- ffpyplayer/setup.py 2024-06-02 11:10:49.691183467 +0530
|
||||
+++ ffpyplayer.mod/setup.py 2024-06-02 11:20:16.220966873 +0530
|
||||
@@ -27,12 +27,6 @@
|
||||
# This sets whether or not Cython gets added to setup_requires.
|
||||
declare_cython = False
|
||||
|
||||
-if platform in ('ios', 'android'):
|
||||
- # NEVER use or declare cython on these platforms
|
||||
- print('Not using cython on %s' % platform)
|
||||
- can_use_cython = False
|
||||
-else:
|
||||
- declare_cython = True
|
||||
|
||||
src_path = build_path = dirname(__file__)
|
||||
print(f'Source/build path: {src_path}')
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
import sh
|
||||
import os
|
||||
|
||||
from multiprocessing import cpu_count
|
||||
from pathlib import Path
|
||||
from os.path import join
|
||||
|
||||
from packaging.version import Version
|
||||
from pythonforandroid.logger import shprint
|
||||
from pythonforandroid.recipe import Recipe
|
||||
from pythonforandroid.util import (
|
||||
BuildInterruptingException,
|
||||
current_directory,
|
||||
ensure_dir,
|
||||
)
|
||||
from pythonforandroid.prerequisites import OpenSSLPrerequisite
|
||||
|
||||
HOSTPYTHON_VERSION_UNSET_MESSAGE = (
|
||||
'The hostpython recipe must have set version'
|
||||
)
|
||||
|
||||
SETUP_DIST_NOT_FIND_MESSAGE = (
|
||||
'Could not find Setup.dist or Setup in Python build'
|
||||
)
|
||||
|
||||
|
||||
class HostPython3Recipe(Recipe):
|
||||
'''
|
||||
The hostpython3's recipe.
|
||||
|
||||
.. versionchanged:: 2019.10.06.post0
|
||||
Refactored from deleted class ``python.HostPythonRecipe`` into here.
|
||||
|
||||
.. versionchanged:: 0.6.0
|
||||
Refactored into the new class
|
||||
:class:`~pythonforandroid.python.HostPythonRecipe`
|
||||
'''
|
||||
|
||||
version = '3.11.5'
|
||||
name = 'hostpython3'
|
||||
|
||||
build_subdir = 'native-build'
|
||||
'''Specify the sub build directory for the hostpython3 recipe. Defaults
|
||||
to ``native-build``.'''
|
||||
|
||||
url = 'https://www.python.org/ftp/python/{version}/Python-{version}.tgz'
|
||||
'''The default url to download our host python recipe. This url will
|
||||
change depending on the python version set in attribute :attr:`version`.'''
|
||||
|
||||
patches = ['patches/pyconfig_detection.patch']
|
||||
|
||||
@property
|
||||
def _exe_name(self):
|
||||
'''
|
||||
Returns the name of the python executable depending on the version.
|
||||
'''
|
||||
if not self.version:
|
||||
raise BuildInterruptingException(HOSTPYTHON_VERSION_UNSET_MESSAGE)
|
||||
return f'python{self.version.split(".")[0]}'
|
||||
|
||||
@property
|
||||
def python_exe(self):
|
||||
'''Returns the full path of the hostpython executable.'''
|
||||
return join(self.get_path_to_python(), self._exe_name)
|
||||
|
||||
def get_recipe_env(self, arch=None):
|
||||
env = os.environ.copy()
|
||||
openssl_prereq = OpenSSLPrerequisite()
|
||||
if env.get("PKG_CONFIG_PATH", ""):
|
||||
env["PKG_CONFIG_PATH"] = os.pathsep.join(
|
||||
[openssl_prereq.pkg_config_location, env["PKG_CONFIG_PATH"]]
|
||||
)
|
||||
else:
|
||||
env["PKG_CONFIG_PATH"] = openssl_prereq.pkg_config_location
|
||||
return env
|
||||
|
||||
def should_build(self, arch):
|
||||
if Path(self.python_exe).exists():
|
||||
# no need to build, but we must set hostpython for our Context
|
||||
self.ctx.hostpython = self.python_exe
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_build_container_dir(self, arch=None):
|
||||
choices = self.check_recipe_choices()
|
||||
dir_name = '-'.join([self.name] + choices)
|
||||
return join(self.ctx.build_dir, 'other_builds', dir_name, 'desktop')
|
||||
|
||||
def get_build_dir(self, arch=None):
|
||||
'''
|
||||
.. note:: Unlike other recipes, the hostpython build dir doesn't
|
||||
depend on the target arch
|
||||
'''
|
||||
return join(self.get_build_container_dir(), self.name)
|
||||
|
||||
def get_path_to_python(self):
|
||||
return join(self.get_build_dir(), self.build_subdir)
|
||||
|
||||
@property
|
||||
def site_root(self):
|
||||
return join(self.get_path_to_python(), "root")
|
||||
|
||||
@property
|
||||
def site_bin(self):
|
||||
return join(self.site_root, self.site_dir, "bin")
|
||||
|
||||
@property
|
||||
def local_bin(self):
|
||||
return join(self.site_root, "usr/local/bin/")
|
||||
|
||||
@property
|
||||
def site_dir(self):
|
||||
p_version = Version(self.version)
|
||||
return join(
|
||||
self.site_root,
|
||||
f"usr/local/lib/python{p_version.major}.{p_version.minor}/site-packages/"
|
||||
)
|
||||
|
||||
def build_arch(self, arch):
|
||||
env = self.get_recipe_env(arch)
|
||||
|
||||
recipe_build_dir = self.get_build_dir(arch.arch)
|
||||
|
||||
# Create a subdirectory to actually perform the build
|
||||
build_dir = join(recipe_build_dir, self.build_subdir)
|
||||
ensure_dir(build_dir)
|
||||
|
||||
# Configure the build
|
||||
build_configured = False
|
||||
with current_directory(build_dir):
|
||||
if not Path('config.status').exists():
|
||||
shprint(sh.Command(join(recipe_build_dir, 'configure')), _env=env)
|
||||
build_configured = True
|
||||
|
||||
with current_directory(recipe_build_dir):
|
||||
# Create the Setup file. This copying from Setup.dist is
|
||||
# the normal and expected procedure before Python 3.8, but
|
||||
# after this the file with default options is already named "Setup"
|
||||
setup_dist_location = join('Modules', 'Setup.dist')
|
||||
if Path(setup_dist_location).exists():
|
||||
shprint(sh.cp, setup_dist_location,
|
||||
join(build_dir, 'Modules', 'Setup'))
|
||||
else:
|
||||
# Check the expected file does exist
|
||||
setup_location = join('Modules', 'Setup')
|
||||
if not Path(setup_location).exists():
|
||||
raise BuildInterruptingException(
|
||||
SETUP_DIST_NOT_FIND_MESSAGE
|
||||
)
|
||||
|
||||
shprint(sh.make, '-j', str(cpu_count()), '-C', build_dir, _env=env)
|
||||
|
||||
# make a copy of the python executable giving it the name we want,
|
||||
# because we got different python's executable names depending on
|
||||
# the fs being case-insensitive (Mac OS X, Cygwin...) or
|
||||
# case-sensitive (linux)...so this way we will have an unique name
|
||||
# for our hostpython, regarding the used fs
|
||||
for exe_name in ['python.exe', 'python']:
|
||||
exe = join(self.get_path_to_python(), exe_name)
|
||||
if Path(exe).is_file():
|
||||
shprint(sh.cp, exe, self.python_exe)
|
||||
break
|
||||
|
||||
ensure_dir(self.site_root)
|
||||
self.ctx.hostpython = self.python_exe
|
||||
if build_configured:
|
||||
print("RUNNING ENSUREPIP:"+self.site_root)
|
||||
shprint(
|
||||
sh.Command(self.python_exe), "-m", "ensurepip", "--root", self.site_root, "-U",
|
||||
_env={"HOME": "/tmp"}
|
||||
)
|
||||
print("RAN ENSUREPIP")
|
||||
|
||||
|
||||
recipe = HostPython3Recipe()
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
diff -Nru Python-3.8.2/Lib/site.py Python-3.8.2-new/Lib/site.py
|
||||
--- Python-3.8.2/Lib/site.py 2020-04-28 12:48:38.000000000 -0700
|
||||
+++ Python-3.8.2-new/Lib/site.py 2020-04-28 12:52:46.000000000 -0700
|
||||
@@ -487,7 +487,8 @@
|
||||
if key == 'include-system-site-packages':
|
||||
system_site = value.lower()
|
||||
elif key == 'home':
|
||||
- sys._home = value
|
||||
+ # this is breaking pyconfig.h path detection with venv
|
||||
+ print('Ignoring "sys._home = value" override', file=sys.stderr)
|
||||
|
||||
sys.prefix = sys.exec_prefix = site_prefix
|
||||
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
APP_OPTIM := release
|
||||
APP_ABI := all # or armeabi
|
||||
APP_MODULES := libjpeg
|
||||
APP_ALLOW_MISSING_DEPS := true
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
from pythonforandroid.recipe import Recipe
|
||||
from pythonforandroid.logger import shprint
|
||||
from pythonforandroid.util import current_directory
|
||||
from os.path import join
|
||||
import sh
|
||||
|
||||
|
||||
class JpegRecipe(Recipe):
|
||||
'''
|
||||
.. versionchanged:: 0.6.0
|
||||
rewrote recipe to be build with clang and updated libraries to latest
|
||||
version of the official git repo.
|
||||
'''
|
||||
name = 'jpeg'
|
||||
version = '2.0.1'
|
||||
url = 'https://github.com/libjpeg-turbo/libjpeg-turbo/archive/{version}.tar.gz' # noqa
|
||||
built_libraries = {'libjpeg.a': '.', 'libturbojpeg.a': '.'}
|
||||
# we will require this below patch to build the shared library
|
||||
# patches = ['remove-version.patch']
|
||||
|
||||
def build_arch(self, arch):
|
||||
build_dir = self.get_build_dir(arch.arch)
|
||||
|
||||
# TODO: Fix simd/neon
|
||||
with current_directory(build_dir):
|
||||
env = self.get_recipe_env(arch)
|
||||
toolchain_file = join(self.ctx.ndk_dir,
|
||||
'build/cmake/android.toolchain.cmake')
|
||||
|
||||
shprint(sh.rm, '-rf', 'CMakeCache.txt', 'CMakeFiles/')
|
||||
shprint(sh.cmake, '-G', 'Unix Makefiles',
|
||||
'-DCMAKE_SYSTEM_NAME=Android',
|
||||
'-DCMAKE_POSITION_INDEPENDENT_CODE=1',
|
||||
'-DCMAKE_ANDROID_ARCH_ABI={arch}'.format(arch=arch.arch),
|
||||
'-DCMAKE_ANDROID_NDK=' + self.ctx.ndk_dir,
|
||||
'-DCMAKE_C_COMPILER={cc}'.format(cc=arch.get_clang_exe()),
|
||||
'-DCMAKE_CXX_COMPILER={cc_plus}'.format(
|
||||
cc_plus=arch.get_clang_exe(plus_plus=True)),
|
||||
'-DCMAKE_BUILD_TYPE=Release',
|
||||
'-DCMAKE_INSTALL_PREFIX=./install',
|
||||
'-DCMAKE_TOOLCHAIN_FILE=' + toolchain_file,
|
||||
|
||||
'-DANDROID_ABI={arch}'.format(arch=arch.arch),
|
||||
'-DANDROID_ARM_NEON=ON',
|
||||
'-DENABLE_NEON=ON',
|
||||
# '-DREQUIRE_SIMD=1',
|
||||
|
||||
# Force disable shared, with the static ones is enough
|
||||
'-DENABLE_SHARED=0',
|
||||
'-DENABLE_STATIC=1',
|
||||
|
||||
# Fix cmake compatibility issue
|
||||
'-DCMAKE_POLICY_VERSION_MINIMUM=3.5',
|
||||
_env=env)
|
||||
shprint(sh.make, _env=env)
|
||||
|
||||
|
||||
recipe = JpegRecipe()
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
diff -Naur jpeg/Android.mk b/Android.mk
|
||||
--- jpeg/Android.mk 2015-12-14 11:37:25.900190235 -0600
|
||||
+++ b/Android.mk 2015-12-14 11:41:27.532182210 -0600
|
||||
@@ -54,8 +54,7 @@
|
||||
|
||||
LOCAL_SRC_FILES:= $(libjpeg_SOURCES_DIST)
|
||||
|
||||
-LOCAL_SHARED_LIBRARIES := libcutils
|
||||
-LOCAL_STATIC_LIBRARIES := libsimd
|
||||
+LOCAL_STATIC_LIBRARIES := libsimd libcutils
|
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)
|
||||
|
||||
@@ -68,7 +67,7 @@
|
||||
|
||||
LOCAL_MODULE := libjpeg
|
||||
|
||||
-include $(BUILD_SHARED_LIBRARY)
|
||||
+include $(BUILD_STATIC_LIBRARY)
|
||||
|
||||
######################################################
|
||||
### cjpeg ###
|
||||
@@ -82,7 +81,7 @@
|
||||
|
||||
LOCAL_SRC_FILES:= $(cjpeg_SOURCES)
|
||||
|
||||
-LOCAL_SHARED_LIBRARIES := libjpeg
|
||||
+LOCAL_STATIC_LIBRARIES := libjpeg
|
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH) \
|
||||
$(LOCAL_PATH)/android
|
||||
@@ -110,7 +109,7 @@
|
||||
|
||||
LOCAL_SRC_FILES:= $(djpeg_SOURCES)
|
||||
|
||||
-LOCAL_SHARED_LIBRARIES := libjpeg
|
||||
+LOCAL_STATIC_LIBRARIES := libjpeg
|
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH) \
|
||||
$(LOCAL_PATH)/android
|
||||
@@ -137,7 +136,7 @@
|
||||
|
||||
LOCAL_SRC_FILES:= $(jpegtran_SOURCES)
|
||||
|
||||
-LOCAL_SHARED_LIBRARIES := libjpeg
|
||||
+LOCAL_STATIC_LIBRARIES := libjpeg
|
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH) \
|
||||
$(LOCAL_PATH)/android
|
||||
@@ -163,7 +162,7 @@
|
||||
|
||||
LOCAL_SRC_FILES:= $(tjunittest_SOURCES)
|
||||
|
||||
-LOCAL_SHARED_LIBRARIES := libjpeg
|
||||
+LOCAL_STATIC_LIBRARIES := libjpeg
|
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)
|
||||
|
||||
@@ -189,7 +188,7 @@
|
||||
|
||||
LOCAL_SRC_FILES:= $(tjbench_SOURCES)
|
||||
|
||||
-LOCAL_SHARED_LIBRARIES := libjpeg
|
||||
+LOCAL_STATIC_LIBRARIES := libjpeg
|
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)
|
||||
|
||||
@@ -215,7 +214,7 @@
|
||||
|
||||
LOCAL_SRC_FILES:= $(rdjpgcom_SOURCES)
|
||||
|
||||
-LOCAL_SHARED_LIBRARIES := libjpeg
|
||||
+LOCAL_STATIC_LIBRARIES := libjpeg
|
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)
|
||||
|
||||
@@ -240,7 +239,7 @@
|
||||
|
||||
LOCAL_SRC_FILES:= $(wrjpgcom_SOURCES)
|
||||
|
||||
-LOCAL_SHARED_LIBRARIES := libjpeg
|
||||
+LOCAL_STATIC_LIBRARIES := libjpeg
|
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)
|
||||
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
--- jpeg/CMakeLists.txt.orig 2018-11-12 20:20:28.000000000 +0100
|
||||
+++ jpeg/CMakeLists.txt 2018-12-14 12:43:45.338704504 +0100
|
||||
@@ -573,6 +573,9 @@
|
||||
add_library(turbojpeg SHARED ${TURBOJPEG_SOURCES})
|
||||
set_property(TARGET turbojpeg PROPERTY COMPILE_FLAGS
|
||||
"-DBMP_SUPPORTED -DPPM_SUPPORTED")
|
||||
+ set_property(TARGET jpeg PROPERTY NO_SONAME 1)
|
||||
+ set_property(TARGET turbojpeg PROPERTY NO_SONAME 1)
|
||||
+ set(CMAKE_SHARED_LIBRARY_SONAME_C_FLAG "")
|
||||
if(WIN32)
|
||||
set_target_properties(turbojpeg PROPERTIES DEFINE_SYMBOL DLLDEFINE)
|
||||
endif()
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
"""
|
||||
Android Bluetooth Low Energy
|
||||
"""
|
||||
from pythonforandroid.recipe import PythonRecipe
|
||||
from pythonforandroid.toolchain import current_directory, info, shprint
|
||||
import sh
|
||||
from os.path import join
|
||||
|
||||
|
||||
class LXSTRecipe(PythonRecipe):
|
||||
name = 'lxst_recipe'
|
||||
depends = ['python3', 'setuptools', 'android', "cffi"]
|
||||
call_hostpython_via_targetpython = False
|
||||
install_in_hostpython = True
|
||||
|
||||
def prepare_build_dir(self, arch):
|
||||
build_dir = self.get_build_dir(arch)
|
||||
assert build_dir.endswith(self.name)
|
||||
shprint(sh.rm, '-rf', build_dir)
|
||||
shprint(sh.mkdir, build_dir)
|
||||
|
||||
srcs = ('/home/markqvist/Information/Source/LXST/LXST', '/home/markqvist/Information/Source/LXST/setup.py', '/home/markqvist/Information/Source/LXST/README.md')
|
||||
|
||||
for filename in srcs:
|
||||
print(f"Copy {join(self.get_recipe_dir(), filename)} to {build_dir}")
|
||||
shprint(sh.cp, '-a', join(self.get_recipe_dir(), filename),
|
||||
build_dir)
|
||||
|
||||
def postbuild_arch(self, arch):
|
||||
super(LXSTRecipe, self).postbuild_arch(arch)
|
||||
info("LXST native build completed")
|
||||
|
||||
recipe = LXSTRecipe()
|
||||
152
recipes/mffmpeg/__init__.py
Normal file
152
recipes/mffmpeg/__init__.py
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
from pythonforandroid.toolchain import Recipe, current_directory, shprint
|
||||
from os.path import exists, join, realpath
|
||||
import sh
|
||||
|
||||
|
||||
class FFMpegRecipe(Recipe):
|
||||
version = 'n4.3.1'
|
||||
# Moved to github.com instead of ffmpeg.org to improve download speed
|
||||
url = 'https://github.com/FFmpeg/FFmpeg/archive/{version}.zip'
|
||||
depends = ['sdl2'] # Need this to build correct recipe order
|
||||
opts_depends = ['openssl', 'ffpyplayer_codecs']
|
||||
patches = ['patches/configure.patch']
|
||||
|
||||
def should_build(self, arch):
|
||||
build_dir = self.get_build_dir(arch.arch)
|
||||
return not exists(join(build_dir, 'lib', 'libavcodec.so'))
|
||||
|
||||
def prebuild_arch(self, arch):
|
||||
self.apply_patches(arch)
|
||||
|
||||
def get_recipe_env(self, arch):
|
||||
env = super().get_recipe_env(arch)
|
||||
env['NDK'] = self.ctx.ndk_dir
|
||||
return env
|
||||
|
||||
def build_arch(self, arch):
|
||||
with current_directory(self.get_build_dir(arch.arch)):
|
||||
env = arch.get_env()
|
||||
|
||||
# flags = ['--disable-everything']
|
||||
flags = []
|
||||
cflags = []
|
||||
ldflags = []
|
||||
|
||||
if 'openssl' in self.ctx.recipe_build_order:
|
||||
flags += [
|
||||
'--enable-openssl',
|
||||
'--enable-nonfree',
|
||||
'--enable-protocol=https,tls_openssl',
|
||||
]
|
||||
build_dir = Recipe.get_recipe(
|
||||
'openssl', self.ctx).get_build_dir(arch.arch)
|
||||
cflags += ['-I' + build_dir + '/include/',
|
||||
'-DOPENSSL_API_COMPAT=0x10002000L']
|
||||
ldflags += ['-L' + build_dir]
|
||||
|
||||
if 'ffpyplayer_codecs' in self.ctx.recipe_build_order:
|
||||
# Enable GPL
|
||||
flags += ['--enable-gpl']
|
||||
|
||||
# libx264
|
||||
flags += ['--enable-libx264']
|
||||
build_dir = Recipe.get_recipe(
|
||||
'libx264', self.ctx).get_build_dir(arch.arch)
|
||||
cflags += ['-I' + build_dir + '/include/']
|
||||
ldflags += ['-lx264', '-L' + build_dir + '/lib/']
|
||||
|
||||
# libshine
|
||||
flags += ['--enable-libshine']
|
||||
build_dir = Recipe.get_recipe('libshine', self.ctx).get_build_dir(arch.arch)
|
||||
cflags += ['-I' + build_dir + '/include/']
|
||||
ldflags += ['-lshine', '-L' + build_dir + '/lib/']
|
||||
ldflags += ['-lm']
|
||||
|
||||
# libvpx
|
||||
flags += ['--enable-libvpx']
|
||||
build_dir = Recipe.get_recipe(
|
||||
'libvpx', self.ctx).get_build_dir(arch.arch)
|
||||
cflags += ['-I' + build_dir + '/include/']
|
||||
ldflags += ['-lvpx', '-L' + build_dir + '/lib/']
|
||||
|
||||
# Enable all codecs:
|
||||
flags += [
|
||||
'--enable-parsers',
|
||||
'--enable-decoders',
|
||||
'--enable-encoders',
|
||||
'--enable-muxers',
|
||||
'--enable-demuxers',
|
||||
]
|
||||
else:
|
||||
# Enable codecs only for .mp4:
|
||||
flags += [
|
||||
'--enable-parser=aac,ac3,h261,h264,mpegaudio,mpeg4video,mpegvideo,vc1',
|
||||
'--enable-decoder=aac,h264,mpeg4,mpegvideo',
|
||||
'--enable-muxer=h264,mov,mp4,mpeg2video',
|
||||
'--enable-demuxer=aac,h264,m4v,mov,mpegvideo,vc1,rtsp',
|
||||
]
|
||||
|
||||
# needed to prevent _ffmpeg.so: version node not found for symbol av_init_packet@LIBAVFORMAT_52
|
||||
# /usr/bin/ld: failed to set dynamic section sizes: Bad value
|
||||
flags += [
|
||||
'--disable-symver',
|
||||
]
|
||||
|
||||
# disable binaries / doc
|
||||
flags += [
|
||||
# '--disable-programs',
|
||||
'--disable-doc',
|
||||
]
|
||||
|
||||
# other flags:
|
||||
flags += [
|
||||
'--enable-filter=aresample,resample,crop,adelay,volume,scale',
|
||||
'--enable-protocol=file,http,hls,udp,tcp',
|
||||
'--enable-small',
|
||||
'--enable-hwaccels',
|
||||
'--enable-pic',
|
||||
'--disable-static',
|
||||
'--disable-debug',
|
||||
'--enable-shared',
|
||||
]
|
||||
|
||||
if 'arm64' in arch.arch:
|
||||
arch_flag = 'aarch64'
|
||||
elif 'x86' in arch.arch:
|
||||
arch_flag = 'x86'
|
||||
flags += ['--disable-asm']
|
||||
else:
|
||||
arch_flag = 'arm'
|
||||
|
||||
# android:
|
||||
flags += [
|
||||
'--target-os=android',
|
||||
'--enable-cross-compile',
|
||||
'--cross-prefix={}-'.format(arch.target),
|
||||
'--arch={}'.format(arch_flag),
|
||||
'--strip={}'.format(self.ctx.ndk.llvm_strip),
|
||||
'--sysroot={}'.format(self.ctx.ndk.sysroot),
|
||||
'--enable-neon',
|
||||
'--prefix={}'.format(realpath('.')),
|
||||
]
|
||||
|
||||
if arch_flag == 'arm':
|
||||
cflags += [
|
||||
'-mfpu=vfpv3-d16',
|
||||
'-mfloat-abi=softfp',
|
||||
'-fPIC',
|
||||
]
|
||||
|
||||
env['CFLAGS'] += ' ' + ' '.join(cflags)
|
||||
env['LDFLAGS'] += ' ' + ' '.join(ldflags)
|
||||
|
||||
configure = sh.Command('./configure')
|
||||
shprint(configure, *flags, _env=env)
|
||||
shprint(sh.make, '-j4', _env=env)
|
||||
shprint(sh.make, 'install', _env=env)
|
||||
# copy libs:
|
||||
sh.cp('-a', sh.glob('./lib/lib*.so'),
|
||||
self.ctx.get_libs_dir(arch.arch))
|
||||
|
||||
|
||||
recipe = FFMpegRecipe()
|
||||
11
recipes/mffmpeg/patches/configure.patch
Normal file
11
recipes/mffmpeg/patches/configure.patch
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
--- ./configure 2020-10-11 19:12:16.759760904 +0200
|
||||
+++ ./configure.patch 2020-10-11 19:15:49.059533563 +0200
|
||||
@@ -6361,7 +6361,7 @@
|
||||
enabled librsvg && require_pkg_config librsvg librsvg-2.0 librsvg-2.0/librsvg/rsvg.h rsvg_handle_render_cairo
|
||||
enabled librtmp && require_pkg_config librtmp librtmp librtmp/rtmp.h RTMP_Socket
|
||||
enabled librubberband && require_pkg_config librubberband "rubberband >= 1.8.1" rubberband/rubberband-c.h rubberband_new -lstdc++ && append librubberband_extralibs "-lstdc++"
|
||||
-enabled libshine && require_pkg_config libshine shine shine/layer3.h shine_encode_buffer
|
||||
+enabled libshine && require "shine" shine/layer3.h shine_encode_buffer -lshine -lm
|
||||
enabled libsmbclient && { check_pkg_config libsmbclient smbclient libsmbclient.h smbc_init ||
|
||||
require libsmbclient libsmbclient.h smbc_init -lsmbclient; }
|
||||
enabled libsnappy && require libsnappy snappy-c.h snappy_compress -lsnappy -lstdc++
|
||||
75
recipes/numpy/__init__.py
Normal file
75
recipes/numpy/__init__.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
from pythonforandroid.recipe import CompiledComponentsPythonRecipe
|
||||
from pythonforandroid.logger import shprint, info
|
||||
from pythonforandroid.util import current_directory
|
||||
from multiprocessing import cpu_count
|
||||
from os.path import join
|
||||
import glob
|
||||
import sh
|
||||
import shutil
|
||||
|
||||
|
||||
class NumpyRecipe(CompiledComponentsPythonRecipe):
|
||||
|
||||
version = '1.22.3'
|
||||
url = 'https://pypi.python.org/packages/source/n/numpy/numpy-{version}.zip'
|
||||
site_packages_name = 'numpy'
|
||||
depends = ['setuptools', 'cython']
|
||||
install_in_hostpython = True
|
||||
call_hostpython_via_targetpython = False
|
||||
|
||||
patches = [
|
||||
join("patches", "remove-default-paths.patch"),
|
||||
join("patches", "add_libm_explicitly_to_build.patch"),
|
||||
join("patches", "ranlib.patch"),
|
||||
]
|
||||
|
||||
def get_recipe_env(self, arch=None, with_flags_in_cc=True):
|
||||
env = super().get_recipe_env(arch, with_flags_in_cc)
|
||||
|
||||
# _PYTHON_HOST_PLATFORM declares that we're cross-compiling
|
||||
# and avoids issues when building on macOS for Android targets.
|
||||
env["_PYTHON_HOST_PLATFORM"] = arch.command_prefix
|
||||
|
||||
# NPY_DISABLE_SVML=1 allows numpy to build for non-AVX512 CPUs
|
||||
# See: https://github.com/numpy/numpy/issues/21196
|
||||
env["NPY_DISABLE_SVML"] = "1"
|
||||
|
||||
return env
|
||||
|
||||
def _build_compiled_components(self, arch):
|
||||
info('Building compiled components in {}'.format(self.name))
|
||||
|
||||
env = self.get_recipe_env(arch)
|
||||
with current_directory(self.get_build_dir(arch.arch)):
|
||||
hostpython = sh.Command(self.hostpython_location)
|
||||
shprint(hostpython, 'setup.py', self.build_cmd, '-v',
|
||||
_env=env, *self.setup_extra_args)
|
||||
build_dir = glob.glob('build/lib.*')[0]
|
||||
shprint(sh.find, build_dir, '-name', '"*.o"', '-exec',
|
||||
env['STRIP'], '{}', ';', _env=env)
|
||||
|
||||
def _rebuild_compiled_components(self, arch, env):
|
||||
info('Rebuilding compiled components in {}'.format(self.name))
|
||||
|
||||
hostpython = sh.Command(self.real_hostpython_location)
|
||||
shprint(hostpython, 'setup.py', 'clean', '--all', '--force', _env=env)
|
||||
shprint(hostpython, 'setup.py', self.build_cmd, '-v', _env=env,
|
||||
*self.setup_extra_args)
|
||||
|
||||
def build_compiled_components(self, arch):
|
||||
self.setup_extra_args = ['-j', str(cpu_count())]
|
||||
self._build_compiled_components(arch)
|
||||
self.setup_extra_args = []
|
||||
|
||||
def rebuild_compiled_components(self, arch, env):
|
||||
self.setup_extra_args = ['-j', str(cpu_count())]
|
||||
self._rebuild_compiled_components(arch, env)
|
||||
self.setup_extra_args = []
|
||||
|
||||
def get_hostrecipe_env(self, arch):
|
||||
env = super().get_hostrecipe_env(arch)
|
||||
env['RANLIB'] = shutil.which('ranlib')
|
||||
return env
|
||||
|
||||
|
||||
recipe = NumpyRecipe()
|
||||
20
recipes/numpy/patches/add_libm_explicitly_to_build.patch
Normal file
20
recipes/numpy/patches/add_libm_explicitly_to_build.patch
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
diff --git a/numpy/linalg/setup.py b/numpy/linalg/setup.py
|
||||
index 66c07c9..d34bd93 100644
|
||||
--- a/numpy/linalg/setup.py
|
||||
+++ b/numpy/linalg/setup.py
|
||||
@@ -46,6 +46,7 @@ def configuration(parent_package='', top_path=None):
|
||||
sources=['lapack_litemodule.c', get_lapack_lite_sources],
|
||||
depends=['lapack_lite/f2c.h'],
|
||||
extra_info=lapack_info,
|
||||
+ libraries=['m'],
|
||||
)
|
||||
|
||||
# umath_linalg module
|
||||
@@ -54,7 +54,7 @@ def configuration(parent_package='', top_path=None):
|
||||
sources=['umath_linalg.c.src', get_lapack_lite_sources],
|
||||
depends=['lapack_lite/f2c.h'],
|
||||
extra_info=lapack_info,
|
||||
- libraries=['npymath'],
|
||||
+ libraries=['npymath', 'm'],
|
||||
)
|
||||
return config
|
||||
11
recipes/numpy/patches/ranlib.patch
Normal file
11
recipes/numpy/patches/ranlib.patch
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
diff -Naur numpy.orig/numpy/distutils/unixccompiler.py numpy/numpy/distutils/unixccompiler.py
|
||||
--- numpy.orig/numpy/distutils/unixccompiler.py 2022-05-28 10:22:10.000000000 +0200
|
||||
+++ numpy/numpy/distutils/unixccompiler.py 2022-05-28 10:22:24.000000000 +0200
|
||||
@@ -124,6 +124,7 @@
|
||||
# platform intelligence here to skip ranlib if it's not
|
||||
# needed -- or maybe Python's configure script took care of
|
||||
# it for us, hence the check for leading colon.
|
||||
+ self.ranlib = [os.environ.get('RANLIB')]
|
||||
if self.ranlib:
|
||||
display = '%s:@ %s' % (os.path.basename(self.ranlib[0]),
|
||||
output_filename)
|
||||
28
recipes/numpy/patches/remove-default-paths.patch
Normal file
28
recipes/numpy/patches/remove-default-paths.patch
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
diff --git a/numpy/distutils/system_info.py b/numpy/distutils/system_info.py
|
||||
index fc7018a..7b514bc 100644
|
||||
--- a/numpy/distutils/system_info.py
|
||||
+++ b/numpy/distutils/system_info.py
|
||||
@@ -340,10 +340,10 @@ if os.path.join(sys.prefix, 'lib') not in default_lib_dirs:
|
||||
default_include_dirs.append(os.path.join(sys.prefix, 'include'))
|
||||
default_src_dirs.append(os.path.join(sys.prefix, 'src'))
|
||||
|
||||
-default_lib_dirs = [_m for _m in default_lib_dirs if os.path.isdir(_m)]
|
||||
-default_runtime_dirs = [_m for _m in default_runtime_dirs if os.path.isdir(_m)]
|
||||
-default_include_dirs = [_m for _m in default_include_dirs if os.path.isdir(_m)]
|
||||
-default_src_dirs = [_m for _m in default_src_dirs if os.path.isdir(_m)]
|
||||
+default_lib_dirs = [] #[_m for _m in default_lib_dirs if os.path.isdir(_m)]
|
||||
+default_runtime_dirs =[] # [_m for _m in default_runtime_dirs if os.path.isdir(_m)]
|
||||
+default_include_dirs =[] # [_m for _m in default_include_dirs if os.path.isdir(_m)]
|
||||
+default_src_dirs =[] # [_m for _m in default_src_dirs if os.path.isdir(_m)]
|
||||
|
||||
so_ext = get_shared_lib_extension()
|
||||
|
||||
@@ -814,7 +814,7 @@ class system_info(object):
|
||||
path = self.get_paths(self.section, key)
|
||||
if path == ['']:
|
||||
path = []
|
||||
- return path
|
||||
+ return []
|
||||
|
||||
def get_include_dirs(self, key='include_dirs'):
|
||||
return self.get_paths(self.section, key)
|
||||
|
|
@ -5,7 +5,7 @@ import sh
|
|||
|
||||
# class PyCodec2Recipe(IncludedFilesBehaviour, CythonRecipe):
|
||||
class PyCodec2Recipe(CythonRecipe):
|
||||
url = "https://github.com/markqvist/pycodec2/archive/438ee4f2f3ee30635a34caddf520cfaccdbbc646.zip"
|
||||
url = "https://github.com/markqvist/pycodec2/archive/refs/heads/main.zip"
|
||||
# src_filename = "../../../pycodec2"
|
||||
depends = ["setuptools", "numpy", "Cython", "codec2"]
|
||||
call_hostpython_via_targetpython = False
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
from pythonforandroid.recipe import PyProjectRecipe
|
||||
from pythonforandroid.toolchain import shprint, current_directory, info
|
||||
from pythonforandroid.patching import will_build
|
||||
import sh
|
||||
from os.path import join
|
||||
|
||||
|
||||
class PyjniusRecipe(PyProjectRecipe):
|
||||
version = '1.7.0'
|
||||
url = 'https://github.com/kivy/pyjnius/archive/{version}.zip'
|
||||
name = 'pyjnius'
|
||||
depends = [('genericndkbuild', 'sdl2', 'sdl3'), 'six']
|
||||
site_packages_name = 'jnius'
|
||||
hostpython_prerequisites = ["Cython<3.2"]
|
||||
patches = [
|
||||
"use_cython.patch",
|
||||
('genericndkbuild_jnienv_getter.patch', will_build('genericndkbuild')),
|
||||
('sdl3_jnienv_getter.patch', will_build('sdl3')),
|
||||
]
|
||||
|
||||
def get_recipe_env(self, arch, **kwargs):
|
||||
env = super().get_recipe_env(arch, **kwargs)
|
||||
|
||||
# Taken from CythonRecipe
|
||||
env['LDFLAGS'] = env['LDFLAGS'] + ' -L{} '.format(
|
||||
self.ctx.get_libs_dir(arch.arch) +
|
||||
' -L{} '.format(self.ctx.libs_dir) +
|
||||
' -L{}'.format(join(self.ctx.bootstrap.build_dir, 'obj', 'local',
|
||||
arch.arch)))
|
||||
env['LDSHARED'] = env['CC'] + ' -shared'
|
||||
env['LIBLINK'] = 'NOTNONE'
|
||||
|
||||
# NDKPLATFORM is our switch for detecting Android platform, so can't be None
|
||||
env['NDKPLATFORM'] = "NOTNONE"
|
||||
return env
|
||||
|
||||
def postbuild_arch(self, arch):
|
||||
super().postbuild_arch(arch)
|
||||
info('Copying pyjnius java class to classes build dir')
|
||||
with current_directory(self.get_build_dir(arch.arch)):
|
||||
shprint(sh.cp, '-a', join('jnius', 'src', 'org'), self.ctx.javaclass_dir)
|
||||
|
||||
|
||||
recipe = PyjniusRecipe()
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
diff -Naur pyjnius.orig/jnius/env.py pyjnius/jnius/env.py
|
||||
--- pyjnius.orig/jnius/env.py 2022-05-28 11:16:02.000000000 +0200
|
||||
+++ pyjnius/jnius/env.py 2022-05-28 11:18:30.000000000 +0200
|
||||
@@ -268,7 +268,7 @@
|
||||
|
||||
class AndroidJavaLocation(UnixJavaLocation):
|
||||
def get_libraries(self):
|
||||
- return ['SDL2', 'log']
|
||||
+ return ['main', 'log']
|
||||
|
||||
def get_include_dirs(self):
|
||||
# When cross-compiling for Android, we should not use the include dirs
|
||||
diff -Naur pyjnius.orig/jnius/jnius_jvm_android.pxi pyjnius/jnius/jnius_jvm_android.pxi
|
||||
--- pyjnius.orig/jnius/jnius_jvm_android.pxi 2022-05-28 11:16:02.000000000 +0200
|
||||
+++ pyjnius/jnius/jnius_jvm_android.pxi 2022-05-28 11:17:17.000000000 +0200
|
||||
@@ -1,6 +1,6 @@
|
||||
# on android, rely on SDL to get the JNI env
|
||||
-cdef extern JNIEnv *SDL_AndroidGetJNIEnv()
|
||||
+cdef extern JNIEnv *WebView_AndroidGetJNIEnv()
|
||||
|
||||
|
||||
cdef JNIEnv *get_platform_jnienv() except NULL:
|
||||
- return <JNIEnv*>SDL_AndroidGetJNIEnv()
|
||||
+ return <JNIEnv*>WebView_AndroidGetJNIEnv()
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
diff -Naur pyjnius.orig/jnius/env.py pyjnius/jnius/env.py
|
||||
--- pyjnius.orig/jnius/env.py 2022-05-28 11:16:02.000000000 +0200
|
||||
+++ pyjnius/jnius/env.py 2022-05-28 11:18:30.000000000 +0200
|
||||
@@ -268,7 +268,7 @@
|
||||
|
||||
class AndroidJavaLocation(UnixJavaLocation):
|
||||
def get_libraries(self):
|
||||
- return ['SDL2', 'log']
|
||||
+ return ['SDL3', 'log']
|
||||
|
||||
def get_include_dirs(self):
|
||||
# When cross-compiling for Android, we should not use the include dirs
|
||||
diff -Naur pyjnius.orig/jnius/jnius_jvm_android.pxi pyjnius/jnius/jnius_jvm_android.pxi
|
||||
--- pyjnius.orig/jnius/jnius_jvm_android.pxi 2022-05-28 11:16:02.000000000 +0200
|
||||
+++ pyjnius/jnius/jnius_jvm_android.pxi 2022-05-28 11:17:17.000000000 +0200
|
||||
@@ -1,6 +1,6 @@
|
||||
# on android, rely on SDL to get the JNI env
|
||||
-cdef extern JNIEnv *SDL_AndroidGetJNIEnv()
|
||||
+cdef extern JNIEnv *SDL_GetAndroidJNIEnv()
|
||||
|
||||
|
||||
cdef JNIEnv *get_platform_jnienv() except NULL:
|
||||
- return <JNIEnv*>SDL_AndroidGetJNIEnv()
|
||||
+ return <JNIEnv*>SDL_GetAndroidJNIEnv()
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
--- pyjnius-1.6.1/setup.py 2023-11-05 21:07:43.000000000 +0530
|
||||
+++ pyjnius-1.6.1.mod/setup.py 2025-03-01 14:47:11.964847337 +0530
|
||||
@@ -59,10 +59,6 @@
|
||||
if NDKPLATFORM is not None and getenv('LIBLINK'):
|
||||
PLATFORM = 'android'
|
||||
|
||||
-# detect platform
|
||||
-if PLATFORM == 'android':
|
||||
- PYX_FILES = [fn[:-3] + 'c' for fn in PYX_FILES]
|
||||
-
|
||||
JAVA=get_java_setup(PLATFORM)
|
||||
|
||||
assert JAVA.is_jdk(), "You need a JDK, we only found a JRE. Try setting JAVA_HOME"
|
||||
|
|
@ -1,445 +0,0 @@
|
|||
import glob
|
||||
import sh
|
||||
import subprocess
|
||||
|
||||
from os import environ, utime
|
||||
from os.path import dirname, exists, join
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
from pythonforandroid.logger import info, warning, shprint
|
||||
from pythonforandroid.patching import version_starts_with
|
||||
from pythonforandroid.recipe import Recipe, TargetPythonRecipe
|
||||
from pythonforandroid.util import (
|
||||
current_directory,
|
||||
ensure_dir,
|
||||
walk_valid_filens,
|
||||
BuildInterruptingException,
|
||||
)
|
||||
|
||||
NDK_API_LOWER_THAN_SUPPORTED_MESSAGE = (
|
||||
'Target ndk-api is {ndk_api}, '
|
||||
'but the python3 recipe supports only {min_ndk_api}+'
|
||||
)
|
||||
|
||||
|
||||
class Python3Recipe(TargetPythonRecipe):
|
||||
'''
|
||||
The python3's recipe
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The python 3 recipe can be built with some extra python modules, but to do
|
||||
so, we need some libraries. By default, we ship the python3 recipe with
|
||||
some common libraries, defined in ``depends``. We also support some optional
|
||||
libraries, which are less common that the ones defined in ``depends``, so
|
||||
we added them as optional dependencies (``opt_depends``).
|
||||
|
||||
Below you have a relationship between the python modules and the recipe
|
||||
libraries::
|
||||
|
||||
- _ctypes: you must add the recipe for ``libffi``.
|
||||
- _sqlite3: you must add the recipe for ``sqlite3``.
|
||||
- _ssl: you must add the recipe for ``openssl``.
|
||||
- _bz2: you must add the recipe for ``libbz2`` (optional).
|
||||
- _lzma: you must add the recipe for ``liblzma`` (optional).
|
||||
|
||||
.. note:: This recipe can be built only against API 21+.
|
||||
|
||||
.. versionchanged:: 2019.10.06.post0
|
||||
- Refactored from deleted class ``python.GuestPythonRecipe`` into here
|
||||
- Added optional dependencies: :mod:`~pythonforandroid.recipes.libbz2`
|
||||
and :mod:`~pythonforandroid.recipes.liblzma`
|
||||
|
||||
.. versionchanged:: 0.6.0
|
||||
Refactored into class
|
||||
:class:`~pythonforandroid.python.GuestPythonRecipe`
|
||||
'''
|
||||
|
||||
version = '3.11.5'
|
||||
url = 'https://www.python.org/ftp/python/{version}/Python-{version}.tgz'
|
||||
name = 'python3'
|
||||
|
||||
patches = [
|
||||
'patches/pyconfig_detection.patch',
|
||||
'patches/reproducible-buildinfo.diff',
|
||||
|
||||
# Python 3.7.1
|
||||
('patches/py3.7.1_fix-ctypes-util-find-library.patch', version_starts_with("3.7")),
|
||||
('patches/py3.7.1_fix-zlib-version.patch', version_starts_with("3.7")),
|
||||
|
||||
# Python 3.8.1 & 3.9.X
|
||||
('patches/py3.8.1.patch', version_starts_with("3.8")),
|
||||
('patches/py3.8.1.patch', version_starts_with("3.9")),
|
||||
('patches/py3.8.1.patch', version_starts_with("3.10")),
|
||||
('patches/cpython-311-ctypes-find-library.patch', version_starts_with("3.11")),
|
||||
]
|
||||
|
||||
if shutil.which('lld') is not None:
|
||||
patches += [
|
||||
("patches/py3.7.1_fix_cortex_a8.patch", version_starts_with("3.7")),
|
||||
("patches/py3.8.1_fix_cortex_a8.patch", version_starts_with("3.8")),
|
||||
("patches/py3.8.1_fix_cortex_a8.patch", version_starts_with("3.9")),
|
||||
("patches/py3.8.1_fix_cortex_a8.patch", version_starts_with("3.10")),
|
||||
("patches/py3.8.1_fix_cortex_a8.patch", version_starts_with("3.11")),
|
||||
]
|
||||
|
||||
depends = ['hostpython3', 'sqlite3', 'openssl', 'libffi']
|
||||
# those optional depends allow us to build python compression modules:
|
||||
# - _bz2.so
|
||||
# - _lzma.so
|
||||
opt_depends = ['libbz2', 'liblzma']
|
||||
'''The optional libraries which we would like to get our python linked'''
|
||||
|
||||
configure_args = (
|
||||
'--host={android_host}',
|
||||
'--build={android_build}',
|
||||
'--enable-shared',
|
||||
'--enable-ipv6',
|
||||
'ac_cv_file__dev_ptmx=yes',
|
||||
'ac_cv_file__dev_ptc=no',
|
||||
'--without-ensurepip',
|
||||
'ac_cv_little_endian_double=yes',
|
||||
'ac_cv_header_sys_eventfd_h=no',
|
||||
'--prefix={prefix}',
|
||||
'--exec-prefix={exec_prefix}',
|
||||
'--enable-loadable-sqlite-extensions'
|
||||
)
|
||||
|
||||
if version_starts_with("3.11"):
|
||||
configure_args += ('--with-build-python={python_host_bin}',)
|
||||
|
||||
'''The configure arguments needed to build the python recipe. Those are
|
||||
used in method :meth:`build_arch` (if not overwritten like python3's
|
||||
recipe does).
|
||||
'''
|
||||
|
||||
MIN_NDK_API = 21
|
||||
'''Sets the minimal ndk api number needed to use the recipe.
|
||||
|
||||
.. warning:: This recipe can be built only against API 21+, so it means
|
||||
that any class which inherits from class:`GuestPythonRecipe` will have
|
||||
this limitation.
|
||||
'''
|
||||
|
||||
stdlib_dir_blacklist = {
|
||||
'__pycache__',
|
||||
'test',
|
||||
'tests',
|
||||
'lib2to3',
|
||||
'ensurepip',
|
||||
'idlelib',
|
||||
'tkinter',
|
||||
}
|
||||
'''The directories that we want to omit for our python bundle'''
|
||||
|
||||
stdlib_filen_blacklist = [
|
||||
'*.py',
|
||||
'*.exe',
|
||||
'*.whl',
|
||||
]
|
||||
'''The file extensions that we want to blacklist for our python bundle'''
|
||||
|
||||
site_packages_dir_blacklist = {
|
||||
'__pycache__',
|
||||
'tests'
|
||||
}
|
||||
'''The directories from site packages dir that we don't want to be included
|
||||
in our python bundle.'''
|
||||
|
||||
site_packages_filen_blacklist = [
|
||||
'*.py'
|
||||
]
|
||||
'''The file extensions from site packages dir that we don't want to be
|
||||
included in our python bundle.'''
|
||||
|
||||
compiled_extension = '.pyc'
|
||||
'''the default extension for compiled python files.
|
||||
|
||||
.. note:: the default extension for compiled python files has been .pyo for
|
||||
python 2.x-3.4 but as of Python 3.5, the .pyo filename extension is no
|
||||
longer used and has been removed in favour of extension .pyc
|
||||
'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._ctx = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def _libpython(self):
|
||||
'''return the python's library name (with extension)'''
|
||||
return 'libpython{link_version}.so'.format(
|
||||
link_version=self.link_version
|
||||
)
|
||||
|
||||
@property
|
||||
def link_version(self):
|
||||
'''return the python's library link version e.g. 3.7m, 3.8'''
|
||||
major, minor = self.major_minor_version_string.split('.')
|
||||
flags = ''
|
||||
if major == '3' and int(minor) < 8:
|
||||
flags += 'm'
|
||||
return '{major}.{minor}{flags}'.format(
|
||||
major=major,
|
||||
minor=minor,
|
||||
flags=flags
|
||||
)
|
||||
|
||||
def include_root(self, arch_name):
|
||||
return join(self.get_build_dir(arch_name), 'Include')
|
||||
|
||||
def link_root(self, arch_name):
|
||||
return join(self.get_build_dir(arch_name), 'android-build')
|
||||
|
||||
def should_build(self, arch):
|
||||
return not Path(self.link_root(arch.arch), self._libpython).is_file()
|
||||
|
||||
def prebuild_arch(self, arch):
|
||||
super().prebuild_arch(arch)
|
||||
self.ctx.python_recipe = self
|
||||
|
||||
def get_recipe_env(self, arch=None, with_flags_in_cc=True):
|
||||
env = super().get_recipe_env(arch)
|
||||
env['HOSTARCH'] = arch.command_prefix
|
||||
|
||||
env['CC'] = arch.get_clang_exe(with_target=True)
|
||||
|
||||
env['PATH'] = (
|
||||
'{hostpython_dir}:{old_path}').format(
|
||||
hostpython_dir=self.get_recipe(
|
||||
'host' + self.name, self.ctx).get_path_to_python(),
|
||||
old_path=env['PATH'])
|
||||
|
||||
env['CFLAGS'] = ' '.join(
|
||||
[
|
||||
'-fPIC',
|
||||
'-DANDROID'
|
||||
]
|
||||
)
|
||||
|
||||
env['LDFLAGS'] = env.get('LDFLAGS', '')
|
||||
if shutil.which('lld') is not None:
|
||||
# Note: The -L. is to fix a bug in python 3.7.
|
||||
# https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234409
|
||||
env['LDFLAGS'] += ' -L. -fuse-ld=lld'
|
||||
else:
|
||||
warning('lld not found, linking without it. '
|
||||
'Consider installing lld if linker errors occur.')
|
||||
|
||||
return env
|
||||
|
||||
def set_libs_flags(self, env, arch):
|
||||
'''Takes care to properly link libraries with python depending on our
|
||||
requirements and the attribute :attr:`opt_depends`.
|
||||
'''
|
||||
def add_flags(include_flags, link_dirs, link_libs):
|
||||
env['CPPFLAGS'] = env.get('CPPFLAGS', '') + include_flags
|
||||
env['LDFLAGS'] = env.get('LDFLAGS', '') + link_dirs
|
||||
env['LIBS'] = env.get('LIBS', '') + link_libs
|
||||
|
||||
if 'sqlite3' in self.ctx.recipe_build_order:
|
||||
info('Activating flags for sqlite3')
|
||||
recipe = Recipe.get_recipe('sqlite3', self.ctx)
|
||||
add_flags(' -I' + recipe.get_build_dir(arch.arch),
|
||||
' -L' + recipe.get_lib_dir(arch), ' -lsqlite3')
|
||||
|
||||
if 'libffi' in self.ctx.recipe_build_order:
|
||||
info('Activating flags for libffi')
|
||||
recipe = Recipe.get_recipe('libffi', self.ctx)
|
||||
# In order to force the correct linkage for our libffi library, we
|
||||
# set the following variable to point where is our libffi.pc file,
|
||||
# because the python build system uses pkg-config to configure it.
|
||||
env['PKG_CONFIG_PATH'] = recipe.get_build_dir(arch.arch)
|
||||
add_flags(' -I' + ' -I'.join(recipe.get_include_dirs(arch)),
|
||||
' -L' + join(recipe.get_build_dir(arch.arch), '.libs'),
|
||||
' -lffi')
|
||||
|
||||
if 'openssl' in self.ctx.recipe_build_order:
|
||||
info('Activating flags for openssl')
|
||||
recipe = Recipe.get_recipe('openssl', self.ctx)
|
||||
self.configure_args += \
|
||||
('--with-openssl=' + recipe.get_build_dir(arch.arch),)
|
||||
add_flags(recipe.include_flags(arch),
|
||||
recipe.link_dirs_flags(arch), recipe.link_libs_flags())
|
||||
|
||||
for library_name in {'libbz2', 'liblzma'}:
|
||||
if library_name in self.ctx.recipe_build_order:
|
||||
info(f'Activating flags for {library_name}')
|
||||
recipe = Recipe.get_recipe(library_name, self.ctx)
|
||||
add_flags(recipe.get_library_includes(arch),
|
||||
recipe.get_library_ldflags(arch),
|
||||
recipe.get_library_libs_flag())
|
||||
|
||||
# python build system contains hardcoded zlib version which prevents
|
||||
# the build of zlib module, here we search for android's zlib version
|
||||
# and sets the right flags, so python can be build with android's zlib
|
||||
info("Activating flags for android's zlib")
|
||||
zlib_lib_path = arch.ndk_lib_dir_versioned
|
||||
zlib_includes = self.ctx.ndk.sysroot_include_dir
|
||||
zlib_h = join(zlib_includes, 'zlib.h')
|
||||
try:
|
||||
with open(zlib_h) as fileh:
|
||||
zlib_data = fileh.read()
|
||||
except IOError:
|
||||
raise BuildInterruptingException(
|
||||
"Could not determine android's zlib version, no zlib.h ({}) in"
|
||||
" the NDK dir includes".format(zlib_h)
|
||||
)
|
||||
for line in zlib_data.split('\n'):
|
||||
if line.startswith('#define ZLIB_VERSION '):
|
||||
break
|
||||
else:
|
||||
raise BuildInterruptingException(
|
||||
'Could not parse zlib.h...so we cannot find zlib version,'
|
||||
'required by python build,'
|
||||
)
|
||||
env['ZLIB_VERSION'] = line.replace('#define ZLIB_VERSION ', '')
|
||||
add_flags(' -I' + zlib_includes, ' -L' + zlib_lib_path, ' -lz')
|
||||
|
||||
return env
|
||||
|
||||
def build_arch(self, arch):
|
||||
if self.ctx.ndk_api < self.MIN_NDK_API:
|
||||
raise BuildInterruptingException(
|
||||
NDK_API_LOWER_THAN_SUPPORTED_MESSAGE.format(
|
||||
ndk_api=self.ctx.ndk_api, min_ndk_api=self.MIN_NDK_API
|
||||
),
|
||||
)
|
||||
|
||||
recipe_build_dir = self.get_build_dir(arch.arch)
|
||||
|
||||
# Create a subdirectory to actually perform the build
|
||||
build_dir = join(recipe_build_dir, 'android-build')
|
||||
ensure_dir(build_dir)
|
||||
|
||||
# TODO: Get these dynamically, like bpo-30386 does
|
||||
sys_prefix = '/usr/local'
|
||||
sys_exec_prefix = '/usr/local'
|
||||
|
||||
env = self.get_recipe_env(arch)
|
||||
env = self.set_libs_flags(env, arch)
|
||||
|
||||
android_build = sh.Command(
|
||||
join(recipe_build_dir,
|
||||
'config.guess'))().strip()
|
||||
|
||||
with current_directory(build_dir):
|
||||
if not exists('config.status'):
|
||||
shprint(
|
||||
sh.Command(join(recipe_build_dir, 'configure')),
|
||||
*(' '.join(self.configure_args).format(
|
||||
android_host=env['HOSTARCH'],
|
||||
android_build=android_build,
|
||||
python_host_bin=join(self.get_recipe(
|
||||
'host' + self.name, self.ctx
|
||||
).get_path_to_python(), "python3"),
|
||||
prefix=sys_prefix,
|
||||
exec_prefix=sys_exec_prefix)).split(' '),
|
||||
_env=env)
|
||||
|
||||
# Python build does not seem to play well with make -j option from Python 3.11 and onwards
|
||||
# Before losing some time, please check issue
|
||||
# https://github.com/python/cpython/issues/101295 , as the root cause looks similar
|
||||
shprint(
|
||||
sh.make,
|
||||
'all',
|
||||
'INSTSONAME={lib_name}'.format(lib_name=self._libpython),
|
||||
_env=env
|
||||
)
|
||||
|
||||
# TODO: Look into passing the path to pyconfig.h in a
|
||||
# better way, although this is probably acceptable
|
||||
sh.cp('pyconfig.h', join(recipe_build_dir, 'Include'))
|
||||
|
||||
def compile_python_files(self, dir):
|
||||
'''
|
||||
Compile the python files (recursively) for the python files inside
|
||||
a given folder.
|
||||
|
||||
.. note:: python2 compiles the files into extension .pyo, but in
|
||||
python3, and as of Python 3.5, the .pyo filename extension is no
|
||||
longer used...uses .pyc (https://www.python.org/dev/peps/pep-0488)
|
||||
'''
|
||||
args = [self.ctx.hostpython]
|
||||
args += ['-OO', '-m', 'compileall', '-b', '-f', dir]
|
||||
subprocess.call(args)
|
||||
|
||||
def create_python_bundle(self, dirn, arch):
|
||||
"""
|
||||
Create a packaged python bundle in the target directory, by
|
||||
copying all the modules and standard library to the right
|
||||
place.
|
||||
"""
|
||||
# Todo: find a better way to find the build libs folder
|
||||
modules_build_dir = join(
|
||||
self.get_build_dir(arch.arch),
|
||||
'android-build',
|
||||
'build',
|
||||
'lib.linux{}-{}-{}'.format(
|
||||
'2' if self.version[0] == '2' else '',
|
||||
arch.command_prefix.split('-')[0],
|
||||
self.major_minor_version_string
|
||||
))
|
||||
|
||||
# Compile to *.pyc the python modules
|
||||
self.compile_python_files(modules_build_dir)
|
||||
# Compile to *.pyc the standard python library
|
||||
self.compile_python_files(join(self.get_build_dir(arch.arch), 'Lib'))
|
||||
# Compile to *.pyc the other python packages (site-packages)
|
||||
self.compile_python_files(self.ctx.get_python_install_dir(arch.arch))
|
||||
|
||||
# Bundle compiled python modules to a folder
|
||||
modules_dir = join(dirn, 'modules')
|
||||
c_ext = self.compiled_extension
|
||||
ensure_dir(modules_dir)
|
||||
module_filens = (glob.glob(join(modules_build_dir, '*.so')) +
|
||||
glob.glob(join(modules_build_dir, '*' + c_ext)))
|
||||
info("Copy {} files into the bundle".format(len(module_filens)))
|
||||
for filen in module_filens:
|
||||
info(" - copy {}".format(filen))
|
||||
shutil.copy2(filen, modules_dir)
|
||||
|
||||
# zip up the standard library
|
||||
stdlib_zip = join(dirn, 'stdlib.zip')
|
||||
with current_directory(join(self.get_build_dir(arch.arch), 'Lib')):
|
||||
stdlib_filens = list(walk_valid_filens(
|
||||
'.', self.stdlib_dir_blacklist, self.stdlib_filen_blacklist))
|
||||
if 'SOURCE_DATE_EPOCH' in environ:
|
||||
# for reproducible builds
|
||||
stdlib_filens.sort()
|
||||
timestamp = int(environ['SOURCE_DATE_EPOCH'])
|
||||
for filen in stdlib_filens:
|
||||
utime(filen, (timestamp, timestamp))
|
||||
info("Zip {} files into the bundle".format(len(stdlib_filens)))
|
||||
shprint(sh.zip, '-X', stdlib_zip, *stdlib_filens)
|
||||
|
||||
# copy the site-packages into place
|
||||
ensure_dir(join(dirn, 'site-packages'))
|
||||
ensure_dir(self.ctx.get_python_install_dir(arch.arch))
|
||||
# TODO: Improve the API around walking and copying the files
|
||||
with current_directory(self.ctx.get_python_install_dir(arch.arch)):
|
||||
filens = list(walk_valid_filens(
|
||||
'.', self.site_packages_dir_blacklist,
|
||||
self.site_packages_filen_blacklist))
|
||||
info("Copy {} files into the site-packages".format(len(filens)))
|
||||
for filen in filens:
|
||||
info(" - copy {}".format(filen))
|
||||
ensure_dir(join(dirn, 'site-packages', dirname(filen)))
|
||||
shutil.copy2(filen, join(dirn, 'site-packages', filen))
|
||||
|
||||
# copy the python .so files into place
|
||||
python_build_dir = join(self.get_build_dir(arch.arch),
|
||||
'android-build')
|
||||
python_lib_name = 'libpython' + self.link_version
|
||||
shprint(
|
||||
sh.cp,
|
||||
join(python_build_dir, python_lib_name + '.so'),
|
||||
join(self.ctx.bootstrap.dist_dir, 'libs', arch.arch)
|
||||
)
|
||||
|
||||
info('Renaming .so files to reflect cross-compile')
|
||||
self.reduce_object_file_names(join(dirn, 'site-packages'))
|
||||
|
||||
return join(dirn, 'site-packages')
|
||||
|
||||
|
||||
recipe = Python3Recipe()
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
--- Python-3.11.5/Lib/ctypes/util.py 2023-08-24 17:39:18.000000000 +0530
|
||||
+++ Python-3.11.5.mod/Lib/ctypes/util.py 2023-11-18 22:12:17.356160615 +0530
|
||||
@@ -4,7 +4,15 @@
|
||||
import sys
|
||||
|
||||
# find_library(name) returns the pathname of a library, or None.
|
||||
-if os.name == "nt":
|
||||
+
|
||||
+# This patch overrides the find_library to look in the right places on
|
||||
+# Android
|
||||
+if True:
|
||||
+ from android._ctypes_library_finder import find_library as _find_lib
|
||||
+ def find_library(name):
|
||||
+ return _find_lib(name)
|
||||
+
|
||||
+elif os.name == "nt":
|
||||
|
||||
def _get_build_version():
|
||||
"""Return the version of MSVC that was used to build Python.
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py
|
||||
--- a/Lib/ctypes/util.py
|
||||
+++ b/Lib/ctypes/util.py
|
||||
@@ -67,4 +67,11 @@
|
||||
return fname
|
||||
return None
|
||||
|
||||
+# This patch overrides the find_library to look in the right places on
|
||||
+# Android
|
||||
+if True:
|
||||
+ from android._ctypes_library_finder import find_library as _find_lib
|
||||
+ def find_library(name):
|
||||
+ return _find_lib(name)
|
||||
+
|
||||
elif os.name == "posix" and sys.platform == "darwin":
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue