z3ugma
01/08/2023, 5:57 AMimport ble
DEV_NAME ::= "O2Ring"
SCAN_DURATION ::= Duration --s=3
find_by_name central/ble.Central name/string:
central.scan --duration=SCAN_DURATION: | device/ble.RemoteScannedDevice |
if device.data.name and device.data.name.contains name:
return device.address
throw "no ring device found"
main:
adapter := ble.Adapter
central := adapter.central
address := find_by_name central DEV_NAME
remote_device := central.connect address
print remote_device.address
services := remote_device.discover_services
//0x0d BLE_HS_ETIMEOUT Operation timed out.
// EXCEPTION error.
// NimBLE error, Type: host, error code: 0x0d. See https://gist.github.com/mikkeldamsgaard/0857ce6a8b073a52...
// 0: ble_get_error_ <sdk>/ble.toit:980:3
// 1: Resource_.throw_error_ <sdk>/ble.toit:811:5
I'm trying to communicate with a ring-based pulse oximeter which does not advertise any services. when calling discover_services
I receive a NimBLE timeout error.
I know the service UUID, and the UUID of the characteristics I'm interested in. Can I skip straight to exchanging write/notify data from the characteristics?
Service data from LightBlue screenshots are attachedfloitsch
01/08/2023, 1:24 PMmikkel.damsgaard
01/08/2023, 2:32 PMmikkel.damsgaard
01/08/2023, 2:39 PMmikkel.damsgaard
01/08/2023, 2:39 PMz3ugma
01/08/2023, 2:57 PMz3ugma
01/08/2023, 2:58 PMdef on_detection(self, device, advertisement_data):
if device.address not in self.devices:
name = device.name or device.address
uuids = device.metadata["uuids"] if "uuids" in device.metadata else None
if self.verbose > 4 and device.address not in self.pipe_down:
print(f"Considering {device.address} {name} {uuids}")
self.pipe_down.append(device.address)
valid = False
if uuids is not None and BLE_MATCH_UUID in uuids and BLE_SERVICE_UUID in uuids:
valid = True
else:
# We might not have the list of UUIDs yet, so also check by name
names = ("Checkme_O2", "CheckO2", "SleepU", "SleepO2", "O2Ring", "WearO2", "KidsO2", "BabyO2", "Oxylink")
for n in names:
if n in name:
if self.verbose > 1:
print(f"Found device by name: {n}")
valid = True
break
if not valid:
return
print(f"Adding device {device.address}")
dev = O2BTDevice(address_or_ble_device=device, timeout=10.0, disconnected_callback=O2BTDevice.on_disconnect)
z3ugma
01/08/2023, 2:59 PMasync def _go_get_services(self):
if self.disconnect_pending or not self.is_connected:
return
# services = await self.get_services()
if self.manager.verbose > 1:
print(f"[{self.name}] Resolved services")
for service in self.services:
print(f"[{self.name}]\tService [{service.uuid}]")
for characteristic in service.characteristics:
print(f"[{self.name}]\t\tCharacteristic [{characteristic.uuid}]")
for descriptor in characteristic.descriptors:
value = await self.read_gatt_descriptor(descriptor.handle)
print(f"[{self.name}]\t\t\tDescriptor [{descriptor.uuid}] ({value})")
for s in self.services:
if s.uuid == BLE_SERVICE_UUID:
for c in s.characteristics:
if c.uuid == BLE_READ_UUID:
asyncio.ensure_future(self._go_enable_notifications(c))
elif c.uuid == BLE_WRITE_UUID:
self.write = c
z3ugma
01/08/2023, 3:03 PMbleak
we call connect
onlyz3ugma
01/08/2023, 3:15 PMself
in this context refers to an instance of BleakClient
class BleakClient:
"""The Client interface for connecting to a specific BLE GATT server and communicating with it.
A BleakClient can be used as an asynchronous context manager in which case it automatically
connects and disconnects.
z3ugma
01/08/2023, 3:19 PMbleak/backends/corebluetooth/PeripheralDelegate.py
then services is populated by peripheral.discoverServices_(None)
which calls into CoreBluetooth
to https://developer.apple.com/documentation/corebluetooth/cbperipheral/1518706-discoverservices/z3ugma
01/08/2023, 3:22 PMdiscover_services
in Toit :
remote_device := central.connect address
print remote_device.address
services := remote_device.discover_services
//0x0d BLE_HS_ETIMEOUT Operation timed out.
// EXCEPTION error.
// NimBLE error, Type: host, error code: 0x0d. See https://gist.github.com/mikkeldamsgaard/0857ce6a8b073a52...
// 0: ble_get_error_ <sdk>/ble.toit:980:3
// 1: Resource_.throw_error_ <sdk>/ble.toit:811:5
^^ I get the commented error, of a bluetooth timeoutz3ugma
01/08/2023, 4:02 PMfloitsch
01/08/2023, 4:28 PMz3ugma
01/08/2023, 4:32 PMfloitsch
01/08/2023, 4:33 PMfloitsch
01/08/2023, 4:33 PMtoit.run
to execute your program.floitsch
01/08/2023, 4:34 PMfloitsch
01/08/2023, 4:34 PMfloitsch
01/08/2023, 4:35 PM~/.cache/jaguar/sdk
floitsch
01/08/2023, 4:35 PMtoit.run
there: ~/.cache/jaguar/sdk/bin/toit.run
.floitsch
01/08/2023, 4:35 PMjag run -d host foo.toit
.floitsch
01/08/2023, 4:36 PMz3ugma
01/08/2023, 4:42 PMjag run -d host blescan.toit
cm started
B6DF069F-1B4D-A7B1-984A-FAFC71D6760A
that works without throwing the timeout exceptionz3ugma
01/08/2023, 4:43 PMprint services
jag run -d host blescan.toit
cm started
B6DF069F-1B4D-A7B1-984A-FAFC71D6760A
[an instance with class-id 47, an instance with class-id 47, an instance with class-id 47]
floitsch
01/08/2023, 4:45 PMfloitsch
01/08/2023, 4:50 PMan instance with class-id 47
is the generic way of printing an object that doesn't have a stringify
method.
We don't store the class names in the executable, to keep the executables smaller.
If you wanted to get the name of the class, you would need to do the following steps:
1. compile the program to a snapshot: ~/.cache/jaguar/sdk/bin/toit.compile -w /tmp/out.snapshot your_program.toit
2. use toitp to print all class-ids: ~/.cache/jaguar/sdk/tools/toitp -c /tmp/out.snapshot
We already discussed that Jaguar (jag run
) should automatically change these "`an instance with class-id XX`" to something more readable, but in theory this could be a text that the user printed and didn't want to be replaced.z3ugma
01/08/2023, 4:54 PMfloitsch
01/08/2023, 4:54 PMmikkel.damsgaard
01/08/2023, 6:35 PMz3ugma
01/08/2023, 6:44 PMmikkel.damsgaard
01/08/2023, 7:00 PMz3ugma
01/08/2023, 7:03 PMz3ugma
01/08/2023, 7:04 PMmikkel.damsgaard
01/08/2023, 7:28 PMz3ugma
01/08/2023, 8:18 PMz3ugma
01/08/2023, 8:19 PMz3ugma
01/08/2023, 8:20 PMfloitsch
01/08/2023, 8:21 PMfloitsch
01/08/2023, 8:22 PMz3ugma
01/08/2023, 8:32 PMmikkel.damsgaard
01/08/2023, 9:28 PMfloitsch
01/08/2023, 9:29 PMz3ugma
01/08/2023, 9:30 PMmikkel.damsgaard
01/08/2023, 9:34 PMz3ugma
01/08/2023, 9:38 PMz3ugma
01/08/2023, 10:12 PMwrite
characteristic of interestmikkel.damsgaard
01/09/2023, 7:45 AM#ifdef ESP_PLATFORM
NimBLEDevice::setPower(ESP_PWR_LVL_P9); /** +9db */
#else
NimBLEDevice::setPower(9); /** +9db */
#endif
z3ugma
01/09/2023, 6:39 PMmikkel.damsgaard
01/09/2023, 6:39 PMmikkel.damsgaard
01/09/2023, 7:45 PMmikkel.damsgaard
01/09/2023, 7:47 PM#define CONFIG_NIMBLE_CPP_LOG_LEVEL 4
I thinkz3ugma
01/09/2023, 8:55 PMCore
log level to DEBUG
as well, I get this more verbose log stack
https://gist.github.com/z3ugma/b50fc6f2cd40729302e8a8c863e13729#file-console_debug-logmikkel.damsgaard
01/09/2023, 9:37 PMmikkel.damsgaard
01/09/2023, 9:49 PMmikkel.damsgaard
01/09/2023, 9:52 PMsleep --ms=500
before the call to discover_services
z3ugma
01/10/2023, 12:34 AM[jaguar] INFO: program 73549571-68fd-594e-80c7-a2904b16810e started
#[0x01, 0xc6, 0xd1, 0x08, 0xef, 0x8a, 0x4d]
[an instance with class-id 48]
[an instance with class-id 45]
[jaguar] INFO: program 73549571-68fd-594e-80c7-a2904b16810e stopped
z3ugma
01/10/2023, 12:35 AMz3ugma
01/10/2023, 2:25 AMwait_for_notification
and subscribe
in the ble library ...and if you're subscribed, is there a place to give a callback function that gets called or run when the notification is sent by the device?z3ugma
01/10/2023, 3:23 AMb'\xaa\x17\xe8\x00\x00\x00\x00\x1b'
This gives the command to the ring to Notify/Indicate to us when the value of the ring next changes.
b'aa17e8000000001b'
[O2Ring 0543] Sending aa17e8000000001b
Writing b'\xaa\x17\xe8\x00\x00\x00\x00\x1b' up to 20 b'\xaa\x17\xe8\x00\x00\x00\x00\x1b'
[O2Ring 0543] Characteristic 8b00ace7-eb0b-49b0-bbe9-9aee0a26e1a3 write value performed
[O2Ring 0543] Characteristic 0734594a-a8e7-4b1a-a6b1-cd5243059a57 updated: 5500ff00000d0061320000000000340000120100
o2pkt recv: 5500ff00000d0061320000000000340000120100
want 21 have 20
[O2Ring 0543] Need more data
[O2Ring 0543] Characteristic 0734594a-a8e7-4b1a-a6b1-cd5243059a57 updated: 07
o2pkt recv: 07
[O2Ring 0543] Final recv: 5500ff00000d006132000000000034000012010007
[O2Ring 0543] 61320000000000340000120100
[O2Ring 0543] SpO2 97%, HR 50 bpm, Perfusion Idx 18, motion 0, batt 52%
mikkel.damsgaard
01/10/2023, 12:16 PMcharacteristics.subscribe // This will subscribe to notifications on the characteristic. The call returns immediately
while true:
notification := characteristics.wait_for_notification // This call blocks until a notification is received. The notification is a ByteArray
print "I received a notification: $notification"
It is common to have the while loop in a task to have it run interleaved:
characteristics.subscribe
task::
while true:
notification := characteristics.wait_for_notification
mikkel.damsgaard
01/10/2023, 12:16 PMunsubscribe
z3ugma
01/14/2023, 6:37 PMz3ugma
01/14/2023, 6:38 PMz3ugma
01/15/2023, 2:45 PMwait_for_notification
but cancel it after a timeout (some sort of nonblocking)
while true:
write_characteristic.write #[0xaa, 0x1b, 0xe4, 0x00, 0x00, 0x01, 0x00, 0x00, 0x5e] // Realtime
first_block := subscribe_characteristic.wait_for_notification //Get first packet
msg_length := first_block[5]
total_length := msg_length + 8
print "$total_length @ $Time.now.utc"
print (hex.encode first_block)
//You'll get 20 bytes at a time
(total_length / 20 ).to_float.floor.to_int.repeat:
notificationline := subscribe_characteristic.wait_for_notification
print (hex.encode notificationline)
sleep --ms=800
z3ugma
01/15/2023, 2:58 PM/**
Waits until the remote device sends a notification or indication on the characteristics. Returns the
notified/indicated value.
See $subscribe.
*/
wait_for_notification -> ByteArray?:
if properties & (CHARACTERISTIC_PROPERTY_INDICATE | CHARACTERISTIC_PROPERTY_NOTIFY) == 0:
throw "Characteristic does not support notifications or indications"
while true:
resource_state_.clear_state VALUE_DATA_READY_EVENT_
buf := ble_get_value_ resource_
if buf: return buf
state := resource_state_.wait_for_state VALUE_DATA_READY_EVENT_ | VALUE_DATA_READ_FAILED_EVENT_ | DISCONNECTED_EVENT_
if state & VALUE_DATA_READ_FAILED_EVENT_ != 0: throw_error_
if state & DISCONNECTED_EVENT_ != 0: throw "Disconnected"
^^ here is how the method is implemented in https://github.com/toitlang/toit/blob/master/lib/ble.toit
Maybe I could extend the class RemoteCharacteristic with another method, wait_for_notification_with_timeout that accepts a timeout parameter and only loops for that many msmikkel.damsgaard
01/15/2023, 9:06 PMmikkel.damsgaard
01/15/2023, 9:08 PMwith_timeout --ms=4000:
data := char.wait_for_notification
That said, I think the BLE library needs to be more aware of disconnects. I am still thinking about how to implement that properly.mikkel.damsgaard
01/16/2023, 7:49 PMz3ugma
01/16/2023, 8:18 PMkasperl
01/18/2023, 8:38 PM