Use local version of able

This commit is contained in:
Mark Qvist 2025-10-29 12:54:59 +01:00
parent 2e44d49d6b
commit 9b6a51a03e
67 changed files with 5305 additions and 0 deletions

1
libs/able/testapps/bletest/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
server

View file

@ -0,0 +1,193 @@
#: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

@ -0,0 +1,17 @@
[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

@ -0,0 +1,245 @@
"""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

@ -0,0 +1,95 @@
// +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 {}
}