Compare commits

..

No commits in common. "main" and "1.5.0" have entirely different histories.
main ... 1.5.0

197 changed files with 18116 additions and 12942 deletions

4
.gitignore vendored
View file

@ -33,7 +33,3 @@ dist
docs/build
sideband*.egg-info
sbapp*.egg-info
LXST
environment
archived_build_tools
.gradle

View file

@ -1,3 +0,0 @@
liberapay: Reticulum
ko_fi: markqvist
custom: "https://unsigned.io/donate"

View file

@ -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.
---
असतो मा सद्गमय
तमसो मा ज्योतिर्गमय
मृत्योर्मा अमृतं गमय

View file

@ -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
View file

@ -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.
![Screenshot](https://github.com/markqvist/Sideband/raw/main/docs/screenshots/devices_small.webp)
@ -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].

View file

@ -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. *"Its 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*

View file

@ -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
View file

@ -1,16 +0,0 @@
.buildozer/
bin/
docs/_build/
*~
*.swp
*.sublime-workspace
*.pyo
*.pyc
*.so
build/
dist/
sdist/
wheels/
*.egg-info

View file

@ -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

View file

@ -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.

View file

@ -1,4 +0,0 @@
include LICENSE
include README.rst
include CHANGELOG.rst
include able/src/org/able/*.java

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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,
]

View file

@ -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()

View file

@ -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')

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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

View file

@ -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]

View file

@ -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'

View file

@ -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."

View file

@ -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

View file

@ -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

View file

@ -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/>`_

View file

@ -1,3 +0,0 @@
.. include:: ../README.rst
.. include:: api.rst
.. include:: example.rst

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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())

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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',
)

View file

@ -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",
},
},
)

View file

@ -1 +0,0 @@
server

View file

@ -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')

View file

@ -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

View file

@ -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()

View file

@ -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 {}
}

View file

@ -1,4 +0,0 @@
there.env
.ipynb_checkpoints/
*.asciidoc
*.ipynb

View file

@ -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: [])
```

View file

@ -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

View file

@ -1,4 +0,0 @@
#!/bin/bash
set -e
for name in test_*.md; do ./run "${name%%.*}"; done

View file

@ -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
----

View file

@ -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]
)
```

View file

@ -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
----

View file

@ -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)
```

View file

@ -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
----

View file

@ -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)
```

View file

@ -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]

View file

@ -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)

View file

@ -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

View file

@ -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")
)

View file

@ -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)

View file

@ -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()

View file

@ -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',
)

View file

@ -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

View file

@ -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()

File diff suppressed because it is too large Load diff

View 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}')

View file

@ -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()

View file

@ -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

View file

@ -1,4 +0,0 @@
APP_OPTIM := release
APP_ABI := all # or armeabi
APP_MODULES := libjpeg
APP_ALLOW_MISSING_DEPS := true

View file

@ -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()

View file

@ -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)

View file

@ -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()

View file

@ -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
View 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()

View 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
View 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()

View 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

View 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)

View 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)

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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"

View file

@ -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()

View file

@ -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.

View file

@ -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