pythonhere 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pythonhere-0.2.1/pythonhere.egg-info → pythonhere-0.2.2}/PKG-INFO +4 -3
- {pythonhere-0.2.1 → pythonhere-0.2.2}/README.rst +2 -1
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pyproject.toml +4 -1
- pythonhere-0.2.2/pythonhere/__init__.py +15 -0
- pythonhere-0.2.2/pythonhere/magic_here/prompts/README.md +49 -0
- pythonhere-0.2.2/pythonhere/magic_here/prompts/able.md +554 -0
- pythonhere-0.2.2/pythonhere/magic_here/prompts/android-media.md +130 -0
- pythonhere-0.2.2/pythonhere/magic_here/prompts/android-packages.md +69 -0
- pythonhere-0.2.2/pythonhere/magic_here/prompts/android-permissions.md +195 -0
- pythonhere-0.2.2/pythonhere/magic_here/prompts/android-runtime.md +34 -0
- pythonhere-0.2.2/pythonhere/magic_here/prompts/jnius.md +432 -0
- pythonhere-0.2.2/pythonhere/magic_here/prompts/kivy-kv.md +239 -0
- pythonhere-0.2.2/pythonhere/magic_here/prompts/kivy-runtime.md +306 -0
- pythonhere-0.2.2/pythonhere/magic_here/prompts/midi.md +243 -0
- pythonhere-0.2.2/pythonhere/magic_here/prompts/plyer.md +202 -0
- pythonhere-0.2.2/pythonhere/magic_here/prompts.py +37 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/main.py +9 -0
- pythonhere-0.2.2/pythonhere/version_here.py +1 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2/pythonhere.egg-info}/PKG-INFO +4 -3
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere.egg-info/SOURCES.txt +13 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere.egg-info/requires.txt +1 -1
- pythonhere-0.2.2/tests/test_ai_prompts.py +57 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/tests/test_main.py +32 -1
- pythonhere-0.2.1/pythonhere/__init__.py +0 -8
- pythonhere-0.2.1/pythonhere/version_here.py +0 -1
- {pythonhere-0.2.1 → pythonhere-0.2.2}/LICENSE +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/android_here.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/data/logo/logo-128.png +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/data/logo/logo-32.png +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/data/logo/logo-splash.png +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/enum_here.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/exception_manager_here.kv +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/exception_manager_here.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/launcher_here.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/magic_here/__init__.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/magic_here/shortcuts.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/network_here.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/patches_here.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/pythonhere.kv +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/server_here.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/ui_here/__init__.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/ui_here/actionbar_here.kv +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/ui_here/common_here.kv +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/ui_here/connection_address_here.kv +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/ui_here/connection_address_here.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/ui_here/layout_here.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/ui_here/server_screen_here.kv +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/ui_here/server_screen_here.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/ui_here/settings_here.kv +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/ui_here/settings_here.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere/window_here.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere.egg-info/dependency_links.txt +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/pythonhere.egg-info/top_level.txt +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/setup.cfg +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/setup.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/tests/test_android_here.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/tests/test_exception_manager_here.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/tests/test_launcher_here.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/tests/test_magic.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/tests/test_network.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/tests/test_patches.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/tests/test_server_here.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/tests/test_settings.py +0 -0
- {pythonhere-0.2.1 → pythonhere-0.2.2}/tests/test_window_here.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonhere
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Here is the Kivy based app to run code from the Jupyter magic %there
|
|
5
5
|
Author-email: b3b <ash.b3b@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
16
16
|
Requires-Python: >=3.10
|
|
17
17
|
Description-Content-Type: text/x-rst
|
|
18
18
|
License-File: LICENSE
|
|
19
|
-
Requires-Dist: herethere[magic]>=0.2.
|
|
19
|
+
Requires-Dist: herethere[magic]>=0.2.3
|
|
20
20
|
Requires-Dist: ipython
|
|
21
21
|
Requires-Dist: ipywidgets
|
|
22
22
|
Requires-Dist: Pillow
|
|
@@ -123,6 +123,7 @@ To build with `Buildozer <https://github.com/kivy/buildozer>`_, run in the sourc
|
|
|
123
123
|
Related resources
|
|
124
124
|
-----------------
|
|
125
125
|
|
|
126
|
-
* `Kivy Remote Shell <https://github.com/kivy/kivy-remote-shell>`_ : Remote SSH+Python interactive shell application
|
|
127
126
|
* `herethere <https://github.com/b3b/herethere>`_ : Library for interactive code execution, based on AsyncSSH
|
|
128
127
|
* `AsyncSSH <https://github.com/ronf/asyncssh>`_ : Asynchronous SSH for Python
|
|
128
|
+
* `Buildozer <https://github.com/kivy/buildozer>`_ : Tool used to build the Android APK.
|
|
129
|
+
* `python-for-android <https://github.com/kivy/python-for-android>`_ : Build toolchain used by Buildozer to package Python applications for Android.
|
|
@@ -99,6 +99,7 @@ To build with `Buildozer <https://github.com/kivy/buildozer>`_, run in the sourc
|
|
|
99
99
|
Related resources
|
|
100
100
|
-----------------
|
|
101
101
|
|
|
102
|
-
* `Kivy Remote Shell <https://github.com/kivy/kivy-remote-shell>`_ : Remote SSH+Python interactive shell application
|
|
103
102
|
* `herethere <https://github.com/b3b/herethere>`_ : Library for interactive code execution, based on AsyncSSH
|
|
104
103
|
* `AsyncSSH <https://github.com/ronf/asyncssh>`_ : Asynchronous SSH for Python
|
|
104
|
+
* `Buildozer <https://github.com/kivy/buildozer>`_ : Tool used to build the Android APK.
|
|
105
|
+
* `python-for-android <https://github.com/kivy/python-for-android>`_ : Build toolchain used by Buildozer to package Python applications for Android.
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
"Programming Language :: Python :: 3.14",
|
|
23
23
|
]
|
|
24
24
|
dependencies = [
|
|
25
|
-
"herethere[magic]>=0.2.
|
|
25
|
+
"herethere[magic]>=0.2.3",
|
|
26
26
|
"ipython",
|
|
27
27
|
"ipywidgets",
|
|
28
28
|
"Pillow",
|
|
@@ -46,6 +46,8 @@ dev = [
|
|
|
46
46
|
]
|
|
47
47
|
docker = [
|
|
48
48
|
"jupytext==1.19.3",
|
|
49
|
+
"pandas",
|
|
50
|
+
"matplotlib",
|
|
49
51
|
]
|
|
50
52
|
|
|
51
53
|
[project.urls]
|
|
@@ -75,6 +77,7 @@ version = { attr = "pythonhere.version_here.__version__" }
|
|
|
75
77
|
pythonhere = [
|
|
76
78
|
"*.kv",
|
|
77
79
|
"data/logo/*.png",
|
|
80
|
+
"magic_here/prompts/*.md",
|
|
78
81
|
"ui_here/*.kv",
|
|
79
82
|
]
|
|
80
83
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""PythonHere Jupyter magic."""
|
|
2
|
+
|
|
3
|
+
from herethere.magic import load_ipython_extension as load_herethere_extension
|
|
4
|
+
|
|
5
|
+
from .magic_here import shortcuts # noqa
|
|
6
|
+
from .magic_here.prompts import register_pythonhere_ai_prompts
|
|
7
|
+
from .version_here import __version__ # noqa
|
|
8
|
+
|
|
9
|
+
__all__ = ("load_ipython_extension",)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_ipython_extension(ipython):
|
|
13
|
+
"""Hook for `%load_extension pythonhere`."""
|
|
14
|
+
register_pythonhere_ai_prompts()
|
|
15
|
+
load_herethere_extension(ipython)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# PythonHere `%%there ai` prompts
|
|
2
|
+
|
|
3
|
+
These Markdown files are PythonHere-specific prompt sections for `%%there ai`.
|
|
4
|
+
They are registered when the `pythonhere` IPython extension is loaded.
|
|
5
|
+
|
|
6
|
+
The prompt sections describe the live PythonHere runtime: Kivy widgets,
|
|
7
|
+
Android/Python-for-Android APIs, Pyjnius, Plyer, runtime permissions, installed
|
|
8
|
+
packages, media access, BLE, and MIDI.
|
|
9
|
+
|
|
10
|
+
## Active by default
|
|
11
|
+
|
|
12
|
+
Normal `%%there ai` requests use these PythonHere sections together with the
|
|
13
|
+
generic [`default`](https://github.com/b3b/herethere/blob/master/herethere/there/ai/prompts/default.md)
|
|
14
|
+
prompt from `herethere`:
|
|
15
|
+
|
|
16
|
+
- [`kivy-runtime`](kivy-runtime.md)
|
|
17
|
+
- [`kivy-kv`](kivy-kv.md)
|
|
18
|
+
- [`android-runtime`](android-runtime.md)
|
|
19
|
+
- [`jnius`](jnius.md)
|
|
20
|
+
- [`android-permissions`](android-permissions.md)
|
|
21
|
+
- [`android-packages`](android-packages.md)
|
|
22
|
+
- [`android-media`](android-media.md)
|
|
23
|
+
- [`plyer`](plyer.md)
|
|
24
|
+
|
|
25
|
+
## Available on request
|
|
26
|
+
|
|
27
|
+
These sections are registered but not active by default. Add them with
|
|
28
|
+
`%%there ai --prompts ...` when a request needs that context:
|
|
29
|
+
|
|
30
|
+
- [`able`](able.md)
|
|
31
|
+
- [`midi`](midi.md)
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
%%there ai --prompts able
|
|
37
|
+
Build a small BLE scanner prototype.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`%%there ai --fix` also uses the `herethere`
|
|
41
|
+
[`fix`](https://github.com/b3b/herethere/blob/master/herethere/there/ai/prompts/fix.md)
|
|
42
|
+
prompt section.
|
|
43
|
+
|
|
44
|
+
## Custom prompts
|
|
45
|
+
|
|
46
|
+
Notebook-specific prompt sections can be added with
|
|
47
|
+
`herethere.there.ai.register_ai_prompt(...)`. Use custom prompts for visual
|
|
48
|
+
style, domain vocabulary, prototype conventions, or other context that should
|
|
49
|
+
not be part of the built-in PythonHere prompt stack.
|
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
## Android BLE using `able`
|
|
2
|
+
|
|
3
|
+
Use `able` package when the user asks for BLE (Bluetooth Low Energy) operations.
|
|
4
|
+
`able` is installed; do not need to check for import errors before normal use.
|
|
5
|
+
|
|
6
|
+
Core API:
|
|
7
|
+
- Import the BLE dispatcher with:
|
|
8
|
+
from able import BluetoothDispatcher
|
|
9
|
+
- Create one shared dispatcher and keep it alive while BLE operations are needed:
|
|
10
|
+
ble = BluetoothDispatcher()
|
|
11
|
+
- Reuse the shared dispatcher across cells:
|
|
12
|
+
if "ble" not in globals() or ble is None:
|
|
13
|
+
ble = BluetoothDispatcher()
|
|
14
|
+
- Close the current GATT client with:
|
|
15
|
+
ble.close_gatt()
|
|
16
|
+
- Start scanning with:
|
|
17
|
+
ble.start_scan()
|
|
18
|
+
- Stop scanning with:
|
|
19
|
+
ble.stop_scan()
|
|
20
|
+
- Connect to a scanned Java BluetoothDevice with:
|
|
21
|
+
ble.connect_gatt(device, autoconnect=False)
|
|
22
|
+
- Connect directly by hardware address with:
|
|
23
|
+
ble.connect_by_device_address(address, autoconnect=False)
|
|
24
|
+
- Discover services after connection with:
|
|
25
|
+
ble.discover_services()
|
|
26
|
+
- Read a characteristic with:
|
|
27
|
+
ble.read_characteristic(characteristic)
|
|
28
|
+
- Write a characteristic with:
|
|
29
|
+
ble.write_characteristic(characteristic, value)
|
|
30
|
+
- Write a descriptor with:
|
|
31
|
+
ble.write_descriptor(descriptor, value)
|
|
32
|
+
- Enable notifications with:
|
|
33
|
+
ble.enable_notifications(characteristic, enable=True, indication=False)
|
|
34
|
+
- Disable notifications with:
|
|
35
|
+
ble.enable_notifications(characteristic, enable=False)
|
|
36
|
+
- Request MTU with:
|
|
37
|
+
ble.request_mtu(mtu)
|
|
38
|
+
- Update RSSI with:
|
|
39
|
+
ble.update_rssi()
|
|
40
|
+
- Change queue timeout with:
|
|
41
|
+
ble.set_queue_timeout(timeout)
|
|
42
|
+
|
|
43
|
+
Important constants:
|
|
44
|
+
- Import common constants when needed:
|
|
45
|
+
from able import GATT_SUCCESS, STATE_CONNECTED, STATE_DISCONNECTED, WriteType
|
|
46
|
+
- `GATT_SUCCESS` is 0.
|
|
47
|
+
- `STATE_CONNECTED` is 2.
|
|
48
|
+
- `STATE_DISCONNECTED` is 0.
|
|
49
|
+
- `WriteType.SIGNED` can be used for signed characteristic writes when requested.
|
|
50
|
+
|
|
51
|
+
BLE dispatcher state:
|
|
52
|
+
- Use a global variable named `ble` for the shared `BluetoothDispatcher`.
|
|
53
|
+
- Do not create a new `BluetoothDispatcher` for every operation.
|
|
54
|
+
- Keep the dispatcher globally inspectable so later cells can run:
|
|
55
|
+
ble.adapter
|
|
56
|
+
ble.bonded_devices
|
|
57
|
+
ble.gatt
|
|
58
|
+
ble.name
|
|
59
|
+
ble.stop_scan()
|
|
60
|
+
ble.close_gatt()
|
|
61
|
+
- Store discovered devices in a global dictionary such as:
|
|
62
|
+
ble_devices_by_address
|
|
63
|
+
- Store the latest services in:
|
|
64
|
+
ble_services
|
|
65
|
+
- Store recent events in:
|
|
66
|
+
ble_events
|
|
67
|
+
- Store recent errors in:
|
|
68
|
+
ble_errors
|
|
69
|
+
- Store compact status messages in:
|
|
70
|
+
ble_status_messages
|
|
71
|
+
- Store the latest characteristic values in:
|
|
72
|
+
ble_characteristic_values
|
|
73
|
+
- Store the latest notification values in:
|
|
74
|
+
ble_notifications
|
|
75
|
+
- Store the latest scan summary in:
|
|
76
|
+
ble_scan_summary
|
|
77
|
+
- Store the currently connected address in:
|
|
78
|
+
ble_connected_address
|
|
79
|
+
- Store the last operation status in:
|
|
80
|
+
ble_last_result
|
|
81
|
+
|
|
82
|
+
Recommended shared initialization:
|
|
83
|
+
from able import BluetoothDispatcher
|
|
84
|
+
|
|
85
|
+
if "ble_events" not in globals():
|
|
86
|
+
ble_events = []
|
|
87
|
+
|
|
88
|
+
if "ble_errors" not in globals():
|
|
89
|
+
ble_errors = []
|
|
90
|
+
|
|
91
|
+
if "ble_status_messages" not in globals():
|
|
92
|
+
ble_status_messages = []
|
|
93
|
+
|
|
94
|
+
if "ble_devices_by_address" not in globals():
|
|
95
|
+
ble_devices_by_address = {}
|
|
96
|
+
|
|
97
|
+
if "ble_characteristic_values" not in globals():
|
|
98
|
+
ble_characteristic_values = {}
|
|
99
|
+
|
|
100
|
+
if "ble_notifications" not in globals():
|
|
101
|
+
ble_notifications = []
|
|
102
|
+
|
|
103
|
+
if "ble_scan_summary" not in globals():
|
|
104
|
+
ble_scan_summary = {}
|
|
105
|
+
|
|
106
|
+
if "ble" not in globals() or ble is None:
|
|
107
|
+
ble = BluetoothDispatcher()
|
|
108
|
+
|
|
109
|
+
Subclassing pattern:
|
|
110
|
+
- For scans, connections, services, reads, writes, notifications, RSSI, and MTU, prefer a small subclass of `BluetoothDispatcher` with event handlers.
|
|
111
|
+
- Reuse an existing subclass instance when possible.
|
|
112
|
+
- Keep callbacks short.
|
|
113
|
+
- Do not rely on `print(...)` inside BLE callbacks as the only output. BLE
|
|
114
|
+
callbacks may run after notebook output capture has ended. Store every event,
|
|
115
|
+
result, and error in globals such as `ble_events`, `ble_errors`,
|
|
116
|
+
`ble_devices_by_address`, `ble_services`, and `ble_notifications`.
|
|
117
|
+
- In BLE callbacks, prefer appending a compact tuple/dict to `ble_events` or a
|
|
118
|
+
feature-specific global over printing. Use `print(...)` only in immediate
|
|
119
|
+
synchronous code that runs before the cell returns.
|
|
120
|
+
- Do not schedule delayed functions whose purpose is to print scan summaries.
|
|
121
|
+
A delayed `Clock.schedule_once(...)` callback may run after notebook output
|
|
122
|
+
capture has ended. Store delayed summaries in globals such as
|
|
123
|
+
`ble_scan_summary` and update visible UI/status messages instead.
|
|
124
|
+
- If the user asked for visible progress, update a Kivy status label or popup
|
|
125
|
+
from callbacks using `Clock.schedule_once(...)`.
|
|
126
|
+
- Store Java objects by address or UUID-like key so later cells can use them.
|
|
127
|
+
- Do not print huge advertisement dumps or service trees directly.
|
|
128
|
+
- Print compact immediate summaries from the initiating cell and store full
|
|
129
|
+
callback results globally.
|
|
130
|
+
|
|
131
|
+
Recommended dispatcher subclass skeleton:
|
|
132
|
+
from able import BluetoothDispatcher, GATT_SUCCESS, STATE_CONNECTED, STATE_DISCONNECTED
|
|
133
|
+
from kivy.clock import Clock
|
|
134
|
+
|
|
135
|
+
def update_ble_status(message):
|
|
136
|
+
ble_status_messages.append(str(message))
|
|
137
|
+
label = globals().get("ble_status_label")
|
|
138
|
+
if label is not None:
|
|
139
|
+
Clock.schedule_once(lambda dt: setattr(label, "text", str(message)), 0)
|
|
140
|
+
|
|
141
|
+
class PythonHereBLE(BluetoothDispatcher):
|
|
142
|
+
def on_scan_started(self, success):
|
|
143
|
+
ble_events.append(("scan_started", bool(success)))
|
|
144
|
+
update_ble_status(f"BLE scan started: {bool(success)}")
|
|
145
|
+
|
|
146
|
+
def on_scan_completed(self):
|
|
147
|
+
ble_events.append(("scan_completed", None))
|
|
148
|
+
update_ble_status("BLE scan completed")
|
|
149
|
+
|
|
150
|
+
def on_device(self, device, rssi, advertisement):
|
|
151
|
+
address = str(device.getAddress())
|
|
152
|
+
try:
|
|
153
|
+
name = device.getName()
|
|
154
|
+
name = str(name) if name is not None else None
|
|
155
|
+
except Exception:
|
|
156
|
+
name = None
|
|
157
|
+
|
|
158
|
+
ble_devices_by_address[address] = {
|
|
159
|
+
"device": device,
|
|
160
|
+
"address": address,
|
|
161
|
+
"name": name,
|
|
162
|
+
"rssi": int(rssi),
|
|
163
|
+
"advertisement": advertisement,
|
|
164
|
+
}
|
|
165
|
+
ble_events.append(("device", address, int(rssi), name))
|
|
166
|
+
update_ble_status(f"{len(ble_devices_by_address)} BLE devices found")
|
|
167
|
+
|
|
168
|
+
def on_connection_state_change(self, status, state):
|
|
169
|
+
global ble_connected_address
|
|
170
|
+
ble_events.append(("connection_state_change", int(status), int(state)))
|
|
171
|
+
|
|
172
|
+
if int(status) == GATT_SUCCESS and int(state) == STATE_CONNECTED:
|
|
173
|
+
ble_connected_address = "connected"
|
|
174
|
+
update_ble_status("BLE connected")
|
|
175
|
+
self.discover_services()
|
|
176
|
+
elif int(state) == STATE_DISCONNECTED:
|
|
177
|
+
ble_connected_address = None
|
|
178
|
+
update_ble_status("BLE disconnected")
|
|
179
|
+
else:
|
|
180
|
+
update_ble_status(f"BLE connection state: {status}, {state}")
|
|
181
|
+
|
|
182
|
+
def on_services(self, services, status):
|
|
183
|
+
global ble_services
|
|
184
|
+
ble_events.append(("services", int(status)))
|
|
185
|
+
if int(status) == GATT_SUCCESS:
|
|
186
|
+
ble_services = services
|
|
187
|
+
update_ble_status("BLE services discovered; stored in ble_services")
|
|
188
|
+
else:
|
|
189
|
+
update_ble_status(f"BLE service discovery failed: {status}")
|
|
190
|
+
|
|
191
|
+
def on_characteristic_read(self, characteristic, status):
|
|
192
|
+
uuid = str(characteristic.getUuid())
|
|
193
|
+
value = list(characteristic.getValue() or [])
|
|
194
|
+
ble_characteristic_values[uuid] = {
|
|
195
|
+
"uuid": uuid,
|
|
196
|
+
"status": int(status),
|
|
197
|
+
"value": value,
|
|
198
|
+
"characteristic": characteristic,
|
|
199
|
+
}
|
|
200
|
+
ble_events.append(("characteristic_read", uuid, int(status), value[:32]))
|
|
201
|
+
update_ble_status(f"Characteristic read: {uuid} status={status}")
|
|
202
|
+
|
|
203
|
+
def on_characteristic_write(self, characteristic, status):
|
|
204
|
+
uuid = str(characteristic.getUuid())
|
|
205
|
+
ble_events.append(("characteristic_write", uuid, int(status)))
|
|
206
|
+
update_ble_status(f"Characteristic write: {uuid} status={status}")
|
|
207
|
+
|
|
208
|
+
def on_characteristic_changed(self, characteristic):
|
|
209
|
+
uuid = str(characteristic.getUuid())
|
|
210
|
+
value = list(characteristic.getValue() or [])
|
|
211
|
+
event = {
|
|
212
|
+
"uuid": uuid,
|
|
213
|
+
"value": value,
|
|
214
|
+
"characteristic": characteristic,
|
|
215
|
+
}
|
|
216
|
+
ble_notifications.append(event)
|
|
217
|
+
ble_events.append(("notification", uuid, value[:32]))
|
|
218
|
+
update_ble_status(f"Notification: {uuid}")
|
|
219
|
+
|
|
220
|
+
def on_descriptor_read(self, descriptor, status):
|
|
221
|
+
uuid = str(descriptor.getUuid())
|
|
222
|
+
ble_events.append(("descriptor_read", uuid, int(status)))
|
|
223
|
+
update_ble_status(f"Descriptor read: {uuid} status={status}")
|
|
224
|
+
|
|
225
|
+
def on_descriptor_write(self, descriptor, status):
|
|
226
|
+
uuid = str(descriptor.getUuid())
|
|
227
|
+
ble_events.append(("descriptor_write", uuid, int(status)))
|
|
228
|
+
update_ble_status(f"Descriptor write: {uuid} status={status}")
|
|
229
|
+
|
|
230
|
+
def on_rssi_updated(self, rssi, status):
|
|
231
|
+
ble_events.append(("rssi_updated", int(rssi), int(status)))
|
|
232
|
+
update_ble_status(f"RSSI: {int(rssi)} status={status}")
|
|
233
|
+
|
|
234
|
+
def on_mtu_changed(self, mtu, status):
|
|
235
|
+
ble_events.append(("mtu_changed", int(mtu), int(status)))
|
|
236
|
+
update_ble_status(f"MTU: {int(mtu)} status={status}")
|
|
237
|
+
|
|
238
|
+
def on_gatt_release(self):
|
|
239
|
+
ble_events.append(("gatt_release", None))
|
|
240
|
+
|
|
241
|
+
def on_error(self, msg):
|
|
242
|
+
msg = str(msg)
|
|
243
|
+
ble_errors.append(msg)
|
|
244
|
+
update_ble_status(f"BLE error: {msg}")
|
|
245
|
+
|
|
246
|
+
if "ble" not in globals() or ble is None or not isinstance(ble, PythonHereBLE):
|
|
247
|
+
ble = PythonHereBLE()
|
|
248
|
+
|
|
249
|
+
Scanning:
|
|
250
|
+
- For simple scan requests, start scanning and schedule `ble.stop_scan()` after a short timeout.
|
|
251
|
+
- Do not scan indefinitely unless the user explicitly asks.
|
|
252
|
+
- Store devices by Bluetooth address in `ble_devices_by_address`.
|
|
253
|
+
- Store compact scan summaries in `ble_scan_summary`.
|
|
254
|
+
- Do not schedule delayed print summaries after scans. Print only one immediate
|
|
255
|
+
line before the cell returns, naming the globals where results will appear.
|
|
256
|
+
- If the user gives a name/address/service/manufacturer filter, use Able filters instead of scanning everything when possible.
|
|
257
|
+
|
|
258
|
+
Recommended simple scan:
|
|
259
|
+
from kivy.clock import Clock
|
|
260
|
+
|
|
261
|
+
def finish_ble_scan(dt):
|
|
262
|
+
global ble_scan_summary
|
|
263
|
+
try:
|
|
264
|
+
ble.stop_scan()
|
|
265
|
+
finally:
|
|
266
|
+
sample = []
|
|
267
|
+
for address, info in list(ble_devices_by_address.items())[:10]:
|
|
268
|
+
sample.append({
|
|
269
|
+
"address": address,
|
|
270
|
+
"name": info.get("name"),
|
|
271
|
+
"rssi": info.get("rssi"),
|
|
272
|
+
})
|
|
273
|
+
ble_scan_summary = {
|
|
274
|
+
"device_count": len(ble_devices_by_address),
|
|
275
|
+
"sample": sample,
|
|
276
|
+
}
|
|
277
|
+
ble_events.append(("scan_summary", ble_scan_summary))
|
|
278
|
+
update_ble_status(
|
|
279
|
+
f"BLE scan complete: {ble_scan_summary['device_count']} device(s)"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
ble.start_scan()
|
|
283
|
+
Clock.schedule_once(finish_ble_scan, 8)
|
|
284
|
+
print("Scanning for 8 seconds. Results will be stored in ble_devices_by_address and ble_scan_summary.")
|
|
285
|
+
|
|
286
|
+
Scan filters:
|
|
287
|
+
- Import filters from `able.filters` when needed:
|
|
288
|
+
from able.filters import (
|
|
289
|
+
EmptyFilter,
|
|
290
|
+
DeviceAddressFilter,
|
|
291
|
+
DeviceNameFilter,
|
|
292
|
+
ManufacturerDataFilter,
|
|
293
|
+
ServiceDataFilter,
|
|
294
|
+
ServiceSolicitationFilter,
|
|
295
|
+
ServiceUUIDFilter,
|
|
296
|
+
)
|
|
297
|
+
- Use `DeviceNameFilter(name)` for exact device-name filtering.
|
|
298
|
+
- Use `DeviceAddressFilter("01:02:03:AB:CD:EF")` for a specific BLE address.
|
|
299
|
+
- Use `ServiceUUIDFilter(uuid)` for service UUID filtering.
|
|
300
|
+
- Use `ManufacturerDataFilter(id, data, mask=None)` for manufacturer data filtering.
|
|
301
|
+
- Use `ServiceDataFilter(uuid, data, mask=None)` for service data filtering.
|
|
302
|
+
- Filters of different kinds can be combined with `&`.
|
|
303
|
+
- Do not combine two filters of the same kind; Able raises `ValueError`.
|
|
304
|
+
- Pass filters as a list to `ble.start_scan(filters=[...])`.
|
|
305
|
+
|
|
306
|
+
Scan settings:
|
|
307
|
+
- Import scan setting builders when the user asks for scan mode, match mode, callback type, or low-latency scan tuning:
|
|
308
|
+
from able.scan_settings import ScanSettingsBuilder, ScanSettings
|
|
309
|
+
- Use `ScanSettingsBuilder()` and builder methods for custom settings.
|
|
310
|
+
- Keep settings simple unless the user asks for advanced tuning.
|
|
311
|
+
|
|
312
|
+
Advertisement parsing:
|
|
313
|
+
- Use `able.Advertisement` objects received by `on_device`.
|
|
314
|
+
- Do not manually parse raw advertisement bytes unless the user asks.
|
|
315
|
+
- For readable summaries, iterate over the advertisement and store parsed AD structures.
|
|
316
|
+
- Keep manufacturer and service data as lists of integers or bytes-like values.
|
|
317
|
+
|
|
318
|
+
Services:
|
|
319
|
+
- `on_services(services, status)` receives an Able `Services` dict-like object.
|
|
320
|
+
- Store it globally as `ble_services`.
|
|
321
|
+
- Use `ble_services.search(pattern)` to find a characteristic by regex pattern.
|
|
322
|
+
- Do not assume a characteristic UUID exists before services are discovered.
|
|
323
|
+
- For user-provided UUID or partial UUID, search `ble_services` first.
|
|
324
|
+
- Store found characteristics in a global such as `ble_characteristics_by_name`.
|
|
325
|
+
|
|
326
|
+
Recommended characteristic search:
|
|
327
|
+
characteristic = ble_services.search("2a37")
|
|
328
|
+
if characteristic is None:
|
|
329
|
+
print("Characteristic not found")
|
|
330
|
+
else:
|
|
331
|
+
print("Characteristic stored in characteristic")
|
|
332
|
+
ble_characteristic = characteristic
|
|
333
|
+
|
|
334
|
+
Connecting:
|
|
335
|
+
- If the user gives a BLE address, use:
|
|
336
|
+
ble.connect_by_device_address(address, autoconnect=False)
|
|
337
|
+
- If the user picks a scanned device, retrieve it from:
|
|
338
|
+
ble_devices_by_address[address]["device"]
|
|
339
|
+
then call:
|
|
340
|
+
ble.connect_gatt(device, autoconnect=False)
|
|
341
|
+
- Validate address-like strings before direct connect when possible.
|
|
342
|
+
- Do not assume connection succeeds immediately; wait for `on_connection_state_change`.
|
|
343
|
+
- After successful connection, call `discover_services()` from the connection callback or instruct the user to run it after connection.
|
|
344
|
+
- Store connected state in globals.
|
|
345
|
+
|
|
346
|
+
Reads and writes:
|
|
347
|
+
- For reads, use a characteristic object from `ble_services.search(...)` or a previously stored characteristic.
|
|
348
|
+
- For writes, accept bytes, bytearray, or list of integers.
|
|
349
|
+
- Keep values in 0..255.
|
|
350
|
+
- Convert simple text only when the user clearly asks to send text.
|
|
351
|
+
- Do not write to characteristics discovered as read-only unless the user explicitly asks to try.
|
|
352
|
+
- Do not repeatedly write in a loop unless the user explicitly asks.
|
|
353
|
+
- Store write attempts and statuses in `ble_events`.
|
|
354
|
+
|
|
355
|
+
Notifications and indications:
|
|
356
|
+
- Use `ble.enable_notifications(characteristic, enable=True, indication=False)` for notifications.
|
|
357
|
+
- Use `indication=True` only when the user asks for indications or when the characteristic is known to require indications.
|
|
358
|
+
- Store received notification values in `ble_notifications`.
|
|
359
|
+
- Provide a disable command:
|
|
360
|
+
ble.enable_notifications(characteristic, enable=False)
|
|
361
|
+
|
|
362
|
+
RSSI and MTU:
|
|
363
|
+
- Use `ble.update_rssi()` when the user asks for signal strength.
|
|
364
|
+
- Use `ble.request_mtu(mtu)` when the user asks to change MTU.
|
|
365
|
+
- Validate MTU as a reasonable positive integer before requesting.
|
|
366
|
+
- Do not assume MTU changed until `on_mtu_changed` reports status.
|
|
367
|
+
|
|
368
|
+
Advertising:
|
|
369
|
+
- Use advertising only when the user asks to advertise, broadcast, become a BLE peripheral advertiser, or send advertisement data.
|
|
370
|
+
- Import advertising helpers when needed:
|
|
371
|
+
from able.advertising import (
|
|
372
|
+
Advertiser,
|
|
373
|
+
AdvertiseData,
|
|
374
|
+
DeviceName,
|
|
375
|
+
TXPowerLevel,
|
|
376
|
+
ServiceUUID,
|
|
377
|
+
ServiceData,
|
|
378
|
+
ManufacturerData,
|
|
379
|
+
Interval,
|
|
380
|
+
TXPower,
|
|
381
|
+
Status,
|
|
382
|
+
)
|
|
383
|
+
- Create one global advertiser reference:
|
|
384
|
+
ble_advertiser
|
|
385
|
+
- Stop existing advertising before starting a new advertiser if appropriate.
|
|
386
|
+
- Keep advertising payload small.
|
|
387
|
+
- Do not include private data in BLE advertisements.
|
|
388
|
+
- Store advertising status in:
|
|
389
|
+
ble_advertising_result
|
|
390
|
+
- Provide a stop helper for advertising:
|
|
391
|
+
def stop_ble_advertising():
|
|
392
|
+
ble_advertiser.stop()
|
|
393
|
+
|
|
394
|
+
Advertising example:
|
|
395
|
+
from able.advertising import Advertiser, AdvertiseData, DeviceName, TXPowerLevel, Interval, TXPower
|
|
396
|
+
|
|
397
|
+
ble_advertiser = Advertiser(
|
|
398
|
+
ble=ble,
|
|
399
|
+
data=AdvertiseData(DeviceName()),
|
|
400
|
+
scan_data=AdvertiseData(TXPowerLevel()),
|
|
401
|
+
interval=Interval.HIGH,
|
|
402
|
+
tx_power=TXPower.MEDIUM,
|
|
403
|
+
)
|
|
404
|
+
ble_advertiser.start()
|
|
405
|
+
print("Started BLE advertising; advertiser stored in ble_advertiser")
|
|
406
|
+
|
|
407
|
+
Permissions:
|
|
408
|
+
- Able methods that require the Bluetooth adapter can request runtime permissions and ask the user to enable Bluetooth.
|
|
409
|
+
- Target API level <= 30 commonly needs `ACCESS_FINE_LOCATION` to obtain BLE scan results.
|
|
410
|
+
- Target API level >= 31 commonly needs `BLUETOOTH_CONNECT`, `BLUETOOTH_SCAN`, `ACCESS_FINE_LOCATION`, and sometimes `BLUETOOTH_ADVERTISE`.
|
|
411
|
+
- Able permission constants are available as:
|
|
412
|
+
from able import Permission
|
|
413
|
+
Permission.ACCESS_FINE_LOCATION
|
|
414
|
+
Permission.ACCESS_BACKGROUND_LOCATION
|
|
415
|
+
Permission.BLUETOOTH_CONNECT
|
|
416
|
+
Permission.BLUETOOTH_SCAN
|
|
417
|
+
Permission.BLUETOOTH_ADVERTISE
|
|
418
|
+
- The requested permission list can be overridden with:
|
|
419
|
+
BluetoothDispatcher(runtime_permissions=[...])
|
|
420
|
+
- Do not duplicate generic Android permission request code here; use Able's dispatcher behavior or the always-enabled Android permissions prompt when the user explicitly asks for permission-specific checks.
|
|
421
|
+
- Do not claim BLE permissions are granted until the relevant operation callback or permission result confirms success.
|
|
422
|
+
|
|
423
|
+
Bluetooth adapter:
|
|
424
|
+
- `ble.adapter` returns the local Android BluetoothAdapter Java object or None.
|
|
425
|
+
- `ble.name` reads or sets the adapter name.
|
|
426
|
+
- `ble.bonded_devices` returns Java BluetoothDevice objects for paired devices.
|
|
427
|
+
- If the adapter is disabled, Able may launch the system activity to let the user enable Bluetooth.
|
|
428
|
+
- Do not assume BLE is available on all devices.
|
|
429
|
+
|
|
430
|
+
Diagnostics:
|
|
431
|
+
- For debugging, print readable compact diagnostics.
|
|
432
|
+
- Useful diagnostics include:
|
|
433
|
+
- whether the shared `ble` object exists,
|
|
434
|
+
- class name of `ble`,
|
|
435
|
+
- `ble.adapter is not None`,
|
|
436
|
+
- `ble.name`,
|
|
437
|
+
- number of `ble.bonded_devices`,
|
|
438
|
+
- number of discovered devices,
|
|
439
|
+
- discovered device addresses/names/RSSI,
|
|
440
|
+
- whether `ble.gatt` is not None,
|
|
441
|
+
- whether `ble_services` exists,
|
|
442
|
+
- recent `ble_events`,
|
|
443
|
+
- recent `ble_errors`,
|
|
444
|
+
- recent notifications.
|
|
445
|
+
- Do not print private data from arbitrary BLE payloads unless the user asks to inspect that payload.
|
|
446
|
+
- Do not access unrelated phone data such as contacts, SMS, call logs, files, camera, microphone, or location for BLE diagnostics.
|
|
447
|
+
|
|
448
|
+
Cleanup:
|
|
449
|
+
- For cleanup, prefer:
|
|
450
|
+
ble.stop_scan()
|
|
451
|
+
ble.close_gatt()
|
|
452
|
+
- For advertising cleanup, call:
|
|
453
|
+
ble_advertiser.stop()
|
|
454
|
+
- For notification cleanup, disable notifications on the characteristic if known.
|
|
455
|
+
- Keep cleanup helpers simple and globally available:
|
|
456
|
+
stop_ble_scan()
|
|
457
|
+
disconnect_ble()
|
|
458
|
+
stop_ble_advertising()
|
|
459
|
+
- Do not set `ble = None` unless the user asks to release the dispatcher itself.
|
|
460
|
+
- Do not leave scans or advertising running without a stop path.
|
|
461
|
+
|
|
462
|
+
Recommended cleanup helpers:
|
|
463
|
+
def stop_ble_scan():
|
|
464
|
+
try:
|
|
465
|
+
ble.stop_scan()
|
|
466
|
+
print("BLE scan stopped")
|
|
467
|
+
except Exception as exc:
|
|
468
|
+
print("Could not stop BLE scan:", repr(exc))
|
|
469
|
+
|
|
470
|
+
def disconnect_ble():
|
|
471
|
+
try:
|
|
472
|
+
ble.close_gatt()
|
|
473
|
+
print("BLE GATT closed")
|
|
474
|
+
except Exception as exc:
|
|
475
|
+
print("Could not close BLE GATT:", repr(exc))
|
|
476
|
+
|
|
477
|
+
def stop_ble_advertising():
|
|
478
|
+
if "ble_advertiser" not in globals() or ble_advertiser is None:
|
|
479
|
+
print("No ble_advertiser global found")
|
|
480
|
+
return
|
|
481
|
+
try:
|
|
482
|
+
ble_advertiser.stop()
|
|
483
|
+
print("BLE advertising stopped")
|
|
484
|
+
except Exception as exc:
|
|
485
|
+
print("Could not stop BLE advertising:", repr(exc))
|
|
486
|
+
|
|
487
|
+
Good command examples:
|
|
488
|
+
- Initialize or reuse:
|
|
489
|
+
from able import BluetoothDispatcher
|
|
490
|
+
|
|
491
|
+
if "ble" not in globals() or ble is None:
|
|
492
|
+
ble = BluetoothDispatcher()
|
|
493
|
+
|
|
494
|
+
- Scan briefly:
|
|
495
|
+
from kivy.clock import Clock
|
|
496
|
+
|
|
497
|
+
ble.start_scan()
|
|
498
|
+
Clock.schedule_once(lambda dt: ble.stop_scan(), 8)
|
|
499
|
+
|
|
500
|
+
- Scan by device name:
|
|
501
|
+
from able.filters import DeviceNameFilter
|
|
502
|
+
from kivy.clock import Clock
|
|
503
|
+
|
|
504
|
+
ble.start_scan(filters=[DeviceNameFilter("MyDevice")])
|
|
505
|
+
Clock.schedule_once(lambda dt: ble.stop_scan(), 8)
|
|
506
|
+
|
|
507
|
+
- Connect by address:
|
|
508
|
+
ble.connect_by_device_address("01:02:03:AB:CD:EF", autoconnect=False)
|
|
509
|
+
|
|
510
|
+
- Connect scanned device:
|
|
511
|
+
device = ble_devices_by_address["01:02:03:AB:CD:EF"]["device"]
|
|
512
|
+
ble.connect_gatt(device, autoconnect=False)
|
|
513
|
+
|
|
514
|
+
- Find a characteristic:
|
|
515
|
+
characteristic = ble_services.search("2a37")
|
|
516
|
+
|
|
517
|
+
- Read a characteristic:
|
|
518
|
+
ble.read_characteristic(characteristic)
|
|
519
|
+
|
|
520
|
+
- Write bytes:
|
|
521
|
+
ble.write_characteristic(characteristic, bytes([1, 2, 3]))
|
|
522
|
+
|
|
523
|
+
- Enable notifications:
|
|
524
|
+
ble.enable_notifications(characteristic, enable=True)
|
|
525
|
+
|
|
526
|
+
- Disable notifications:
|
|
527
|
+
ble.enable_notifications(characteristic, enable=False)
|
|
528
|
+
|
|
529
|
+
- Request MTU:
|
|
530
|
+
ble.request_mtu(247)
|
|
531
|
+
|
|
532
|
+
- Read RSSI:
|
|
533
|
+
ble.update_rssi()
|
|
534
|
+
|
|
535
|
+
Avoid:
|
|
536
|
+
- Do not start endless scans.
|
|
537
|
+
- Do not connect repeatedly in a tight loop.
|
|
538
|
+
- Do not write repeatedly in a tight loop.
|
|
539
|
+
- Do not schedule delayed `print(...)` summaries for scan results.
|
|
540
|
+
- Do not assume scan, connect, services, read, write, notify, RSSI, or MTU operations are synchronous.
|
|
541
|
+
- Do not assume a BLE address, UUID, service, characteristic, or descriptor exists before checking.
|
|
542
|
+
- Do not print huge advertisement dumps, service trees, or notification streams directly.
|
|
543
|
+
- Do not include personal or sensitive data in BLE advertisements.
|
|
544
|
+
|
|
545
|
+
For simple user requests:
|
|
546
|
+
- If the user asks to scan, generate a minimal Able scan cell with a shared dispatcher, event handlers, `ble_devices_by_address`, and a scheduled stop.
|
|
547
|
+
- If the user asks to connect, use a scanned device or direct address and store connection events.
|
|
548
|
+
- If the user asks to list services, call `discover_services()` after connection and store `ble_services`.
|
|
549
|
+
- If the user asks to read, search or use the provided characteristic and call `read_characteristic`.
|
|
550
|
+
- If the user asks to write, validate the value and call `write_characteristic`.
|
|
551
|
+
- If the user asks for notifications, enable notifications and store values in `ble_notifications`.
|
|
552
|
+
- If the user asks to advertise, use `able.advertising` and store `ble_advertiser`.
|
|
553
|
+
- If the user asks for diagnostics, print compact BLE state and recent events.
|
|
554
|
+
- If the user asks for cleanup, stop scan, stop advertising if active, and close GATT.
|