kevincar / bless

Cross-platform Bluetooth Low Energy Server Python Library
MIT License
111 stars 30 forks source link

write_request get executed only after i kill the server #87

Closed arist0v closed 1 year ago

arist0v commented 2 years ago

The problem i created a gatt server based on the gattserverexample.py and i must kill the server(KeyboardInterrupt) to get the write_request code to log on screen

Reproduction

` from concurrent.futures import thread import logging import asyncio import threading

from typing import Any, Dict
from bless import (  # type: ignore
        BlessServer,
        BlessGATTCharacteristic,
        GATTCharacteristicProperties,
        GATTAttributePermissions
        )
GENERIC_ACCESS_SERVICE = "00001800-0000-1000-8000-00805F9B34FB"
DEVICE_NAME = "00002A00-0000-1000-8000-00805F9B34FB"
APPEARANCE = "00002A01-0000-1000-8000-00805F9B34FB"
PERIPH_PREF_CONN_PARAM = "00002A04-0000-1000-8000-00805F9B34FB"

RX_UART = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
TX_UART = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
SERVICE_UART ="6E400001-B5A3-F393-E0A9-E50E24DCCA9E"

GET_ALL_DATAS = 'download mem'
GET_LAST_GPS = 'get position'
ERASE_MEME = 'erase mem'

UART_SAFE_SIZE = 20

DATA_PATTERN = ('ID', 'crc', 'hour', 'minute', 'seconds', 'day', 'month', 'year', 'lat', 'long', 'HDOP', 'time_to_fix', 'nb_sat')

logger = logging.getLogger(__name__)

class BleServer:

    def __init__(self, serviceName) -> None:
        self._serviceName = serviceName
        self._trigger: threading.Event = threading.Event()
        self._gatt: Dict = {
            f"{SERVICE_UART}":{
                f"{RX_UART}":{
                    "Properties": (GATTCharacteristicProperties.write |
                                   GATTCharacteristicProperties.write_without_response)
                },
                f"{TX_UART}": {
                    "Properties": (GATTCharacteristicProperties.notify),
                    "Permissions": (GATTAttributePermissions.readable),
                    "Value": None
                }
            }
        }

    def startServer(self):
        loop = asyncio.get_event_loop()
        logger.debug("Starting Server loop")
        try:
            loop.run_until_complete(self._setServerUp(loop))
        except KeyboardInterrupt:
            loop.run_until_complete(self.stopServer())

    async def _setServerUp(self, loop):
        logger.debug("Setting up server")
        self._trigger.clear()
        self._server = BlessServer(name=self._serviceName, loop=loop)
        #TODO fix service name not showing
        self._server.read_request_func = self.read_request
        self._server.write_request_func = self.write_request
        #logger.debug(self._gatt)
        await self._server.add_gatt(self._gatt)
        logger.debug("Gatt added to the server")
        await self._server.start()
        logger.debug("server started")

        #logger.debug(self._server.get_characteristic(RX_UART))
        logger.debug("advertising")

        self._trigger.wait()
        self._server.get_characteristic(DEVICE_NAME).value = (
            bytearray(self._serviceName.encode())
        )
        self._server.update_value(GENERIC_ACCESS_SERVICE, DEVICE_NAME)
        logger.debug("after wait trigger")

    def read_request(self,
        characteristic: BlessGATTCharacteristic,
        **kwargs
        ) -> bytearray:
        logger.debug(f"Reading {characteristic.value}")
        return characteristic.value

    def write_request(self,
        characteristic: BlessGATTCharacteristic,
        value: Any,
        **kwargs
        ):
        logger.debug("Write Request")
        logger.debug(value)
        characteristic.value = value
        logger.debug(f"Char value set to {characteristic.value}")
        if characteristic.value :
            logger.debug("Nice write request")
            self._trigger.set()

    async def stopServer(self):
        logger.debug("stopping server")
        await self._server.stop()

if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG)
    logger = logging.getLogger(__name__)
    logger.debug("Starting Server in self running mode")
    server = BleServer("GATT_SERVICE")
    server.startServer()

`

Expected behavior I excpected that the write_request section run in real time so i could them code the function triggered by the update of the RX_UART

Screenshots If applicable, add screenshots to help explain your problem.

Desktop (please complete the following information): -Rspbian (rpi 4b 8gb) -Python 3.9.2

Additional context i'm trying to setup a service similar to another one for specific need, this is why i use the Nordic UART service

Other small isue My service name are never advertised, i only got N/A

arist0v commented 2 years ago

additional informatoin:

after some test, look like my trigger.wait never received a set() i'll keep looking so as far as i understand, the wait never stop so the reste of the code never keep going until i ctrl_c

look like wait block the server to manage the write_request

new information:

i also tested with both of the example code, and have the same issue!

kevincar commented 2 years ago

Sorry for the delayed response.

Some systems use threading and coroutines differently. If the code is ran entirely on asynchronous coroutines, my understanding is that blocking the thread will block other coroutines that need to process. To test this: have you tried changing the trigger to be an asyncio.Event rather than a threading.Event? This way, when you await the asyncio.Event the background function for bless can continue working.

arist0v commented 2 years ago

Hello, no worries for the delay, we're all busy ;-)

i'll just try and got the following error:
/home/pi/hotspot-python/pkg/ble/ble_server.py:75: RuntimeWarning: coroutine 'Event.wait' was never awaited

self._trigger.wait()#This one seem to block

Edit: My bad , i'll just add await in front and it's work, but:

Edit 2:

My service are still not advertise when i goes back to threading, so i must look further(i just did some quick fix so maybe i did something wrong) i'll came back to you for this.

if you can help with the device name advertise it would be great)

Edit 3:

look like i wasn'T able to connect to the device at all, so client list remain empty

Edit 4:

i'll try to make my code look more like https://github.com/kevincar/bless/blob/master/examples/server.py but still no service discoverded when i try to connect

kevincar commented 2 years ago

I've edited your code and it runs fine (see below). The await Event.wait() command was blocking the code from getting to the rest of the server setup. The changes below address it. You should see write requests coming through now.

I still need to check why the service name isn't broadcasting. This can be tricky though depending on your system.

import asyncio

import logging
from typing import Any, Dict
from bless import (  # type: ignore
        BlessServer,
        BlessGATTCharacteristic,
        GATTCharacteristicProperties,
        GATTAttributePermissions
        )
GENERIC_ACCESS_SERVICE = "00001800-0000-1000-8000-00805F9B34FB"
DEVICE_NAME = "00002A00-0000-1000-8000-00805F9B34FB"
APPEARANCE = "00002A01-0000-1000-8000-00805F9B34FB"
PERIPH_PREF_CONN_PARAM = "00002A04-0000-1000-8000-00805F9B34FB"

RX_UART = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
TX_UART = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
SERVICE_UART ="6E400001-B5A3-F393-E0A9-E50E24DCCA9E"

GET_ALL_DATAS = 'download mem'
GET_LAST_GPS = 'get position'
ERASE_MEME = 'erase mem'

UART_SAFE_SIZE = 20

DATA_PATTERN = ('ID', 'crc', 'hour', 'minute', 'seconds', 'day', 'month', 'year', 'lat', 'long', 'HDOP', 'time_to_fix', 'nb_sat')

logger = logging.getLogger(__name__)

class BleServer:

    def __init__(self, serviceName) -> None:
        self._serviceName = serviceName
        self._trigger: asyncio.Event = asyncio.Event()
        self._gatt: Dict = {
            f"{SERVICE_UART}":{
                f"{RX_UART}":{
                    "Properties": (GATTCharacteristicProperties.write |
                                   GATTCharacteristicProperties.write_without_response),
                    "Permissions": (GATTAttributePermissions.readable | GATTAttributePermissions.writeable)
                },
                f"{TX_UART}": {
                    "Properties": (GATTCharacteristicProperties.notify),
                    "Permissions": (GATTAttributePermissions.readable),
                    "Value": None
                }
            }
        }

    def startServer(self):
        loop = asyncio.get_event_loop()
        logger.debug("Starting Server loop")
        try:
            loop.run_until_complete(self._setServerUp(loop))
        except KeyboardInterrupt:
            loop.run_until_complete(self.stopServer())

    async def _setServerUp(self, loop):
        logger.debug("Setting up server")
        self._trigger.clear()
        self._server = BlessServer(name=self._serviceName, loop=loop)
        #TODO fix service name not showing
        self._server.read_request_func = self.read_request
        self._server.write_request_func = self.write_request
        #logger.debug(self._gatt)
        await self._server.add_gatt(self._gatt)
        logger.debug("Gatt added to the server")
        await self._server.start()
        logger.debug("server started")

        #logger.debug(self._server.get_characteristic(RX_UART))
        logger.debug("advertising")

        # self._server.get_characteristic(DEVICE_NAME).value = (
            # bytearray(self._serviceName.encode())
        # )
        # self._server.update_value(GENERIC_ACCESS_SERVICE, DEVICE_NAME)
        await self._trigger.wait()
        logger.debug("after wait trigger")

    def read_request(self,
        characteristic: BlessGATTCharacteristic,
        **kwargs
        ) -> bytearray:
        logger.debug(f"Reading {characteristic.value}")
        return characteristic.value

    def write_request(self,
        characteristic: BlessGATTCharacteristic,
        value: Any,
        **kwargs
        ):
        logger.debug("Write Request")
        logger.debug(value)
        characteristic.value = value
        logger.debug(f"Char value set to {characteristic.value}")
        if characteristic.value :
            logger.debug("Nice write request")
            self._trigger.set()

    async def stopServer(self):
        logger.debug("stopping server")
        await self._server.stop()

if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG)
    logger = logging.getLogger(__name__)
    logger.debug("Starting Server in self running mode")
    server = BleServer("GATT_SERVICE")
    server.startServer()
arist0v commented 2 years ago

i tested it, but unfortunately, when trying to connect with nrfConnect, i got disconnected after launching discover service, so i was not able to even try to send a write request, there is still something wrong, i'll copy paste you the last version of my code :

import logging
import asyncio

from typing import Any, Dict
from bless import (  # type: ignore
        BlessServer,
        BlessGATTCharacteristic,
        GATTCharacteristicProperties,
        GATTAttributePermissions
        )

GENERIC_ACCESS_SERVICE = "00001800-0000-1000-8000-00805F9B34FB"
DEVICE_NAME = "00002A00-0000-1000-8000-00805F9B34FB"
APPEARANCE = "00002A01-0000-1000-8000-00805F9B34FB"
PERIPH_PREF_CONN_PARAM = "00002A04-0000-1000-8000-00805F9B34FB"

RX_UART = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
TX_UART = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
SERVICE_UART ="6E400001-B5A3-F393-E0A9-E50E24DCCA9E"

GET_ALL_DATAS = 'download mem'
GET_LAST_GPS = 'get position'
ERASE_MEME = 'erase mem'

UART_SAFE_SIZE = 20

DATA_PATTERN = ('ID', 'crc', 'hour', 'minute', 'seconds', 'day', 'month', 'year', 'lat', 'long', 'HDOP', 'time_to_fix', 'nb_sat')

logger = logging.getLogger(__name__)

class BleServer:

    def __init__(self, serviceName) -> None:
        self._serviceName = serviceName
        self._trigger: asyncio.Event = asyncio.Event()
        self._gatt: Dict = {
            f"{SERVICE_UART}":{
                f"{RX_UART}":{
                    "Properties": (GATTCharacteristicProperties.write |
                                   GATTCharacteristicProperties.write_without_response)
                },
                f"{TX_UART}": {
                    "Properties": (GATTCharacteristicProperties.notify),
                    "Permissions": (GATTAttributePermissions.readable),
                    "Value": None
                }
            }
        }        

    def startServer(self):
        loop = asyncio.get_event_loop()
        logger.debug("Starting Server loop")
        try:
            loop.run_until_complete(self._setServerUp(loop))
        except KeyboardInterrupt:
            loop.run_until_complete(self.stopServer())

    async def _setServerUp(self, loop):

        logger.debug("Setting up server")

        self._trigger.clear()
        self._server = BlessServer(name=self._serviceName, loop=loop)

        #TODO fix service name not showing

        self._server.read_request_func = self.read_request
        self._server.write_request_func = self.write_request

        await self._server.add_gatt(self._gatt)
        logger.debug("Gatt added to the server")
        await self._server.start()
        logger.debug("server started")

        logger.debug("advertising")

        await self._trigger.wait()
        logger.debug("after wait trigger")

    def read_request(self,
        characteristic: BlessGATTCharacteristic,
        **kwargs
        ) -> bytearray:
        logger.debug(f"Reading {characteristic.value}")
        return characteristic.value

    def write_request(self,
        characteristic: BlessGATTCharacteristic,
        value: Any,
        **kwargs
        ):
        logger.debug("Write Request")
        logger.debug(value)
        characteristic.value = value
        logger.debug(f"Char value set to {characteristic.value}")
        if characteristic.value == bytearray(b'allo'):
            self.printSomething()
        elif characteristic.value :
            logger.debug("Nice write request")
            self._trigger.set()

    def printSomething(self):
        logger.debug("TEST")
        print("TEST")

    async def stopServer(self):
        logger.debug("stopping server")
        await self._server.stop()

if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG)
    logger = logging.getLogger(__name__)
    logger.debug("Starting Server in self running mode")
    server = BleServer("GATT_SERVICE")
    server.startServer()
kevincar commented 2 years ago

When you try to connect and discover services, does the python code continue to run or does it crash?

arist0v commented 2 years ago

the python code keep running without issue.

(the version i past earlier)

kevincar commented 2 years ago

Does the examples/server.py example work for you?

arist0v commented 2 years ago

no, i got the same issue

but with a Nordic dev board (so not using python and bless) i receive a gatt service client list on nrfConnect

kevincar commented 2 years ago

Does your raspberry pi have a built in bluetooth module or are you using the nordic board as the adapter?

arist0v commented 2 years ago

O'm using the onboard device, the nordic board are for a linked but dofferent project

Le jeu. 10 nov. 2022, 16 h 00, Kevin Davis @.***> a écrit :

Does your raspberry pi have a built in bluetooth module or are you using the nordic board as the adapter?

— Reply to this email directly, view it on GitHub https://github.com/kevincar/bless/issues/87#issuecomment-1310886788, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABSPAACNLGMCSAZVNARZWRLWHVO6BANCNFSM53VQDGVA . You are receiving this because you authored the thread.Message ID: @.***>

kevincar commented 2 years ago

Any chance I could get a screen ship of the nRF connect error? Do you also have this issue if you use PunchThrough's LightBlue app?

I haven't tested on Raspian, I'll see if I can flash a copy later and test.

arist0v commented 2 years ago

I'll gave you more details later today

Le jeu. 10 nov. 2022, 16 h 21, Kevin Davis @.***> a écrit :

Any chance I could get a screen ship of the nRF connect error? Do you also have this issue if you use PunchThrough's LightBlue app https://punchthrough.com/lightblue/?

I haven't tested on Raspian, I'll see if I can flash a copy later and test.

— Reply to this email directly, view it on GitHub https://github.com/kevincar/bless/issues/87#issuecomment-1310906265, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABSPAAGPSEJ7W53HIQDVARLWHVRNNANCNFSM53VQDGVA . You are receiving this because you authored the thread.Message ID: @.***>

arist0v commented 2 years ago

So i will explain you the whole situation first:

we have the nordic board(dev board) that is use to make a standalone BLE device that my work will use.

in a first stage, i have to code a Android Application that will have to fetch the Device (named puck) data and store it locally until it have network and can be forwarded to our server.(the device will be in a zone with low to no cellular coverage)

in a second stage, i will have to code a Raspberry pi device that will automatically fetch data from the Puck when they are in Ble range (and later probably add LoRa support for better coverage of a site)

So since we will still have the network coverage issue (not cellular data so the Raspberry pi can'T automatically send data) we will have to manually go get the data using an android App, or another raspberry pi that will be mounted on car or stuff.

So my idea to work smarter not hard was to reproduce the puck BLE Behavior (UUID, Command, etc.....) of the Puck so the current android app and raspberry pi python app will be able to fetch the data the exact same way it currently does with the puck.

arist0v commented 2 years ago

Screenshot_20221110-182749.png

There is the screenshot

arist0v commented 2 years ago

And the log from a current Working nordic dev board

nRF Connect, 2022-11-10
AxlStick (C1:50:68:11:28:B7)
D   18:29:06.422    gatt.close()
D   18:29:06.426    wait(200)
V   18:29:06.627    Connecting to C1:50:68:11:28:B7...
D   18:29:06.627    gatt = device.connectGatt(autoConnect = false, TRANSPORT_LE, preferred PHY = LE 1M)
D   18:29:06.766    [Server callback] Connection state changed with status: 0 and new state: CONNECTED (2)
I   18:29:06.766    [Server] Device with address C1:50:68:11:28:B7 connected
I   18:29:06.767    [Server] MTU changed to 247
D   18:29:06.778    [Callback] Connection state changed with status: 0 and new state: CONNECTED (2)
I   18:29:06.778    Connected to C1:50:68:11:28:B7
D   18:29:06.786    [Broadcast] Action received: android.bluetooth.device.action.ACL_CONNECTED
V   18:29:06.796    Discovering services...
D   18:29:06.796    gatt.discoverServices()
I   18:29:07.472    Connection parameters updated (interval: 7.5ms, latency: 0, timeout: 5000ms)
D   18:29:07.703    [Callback] Services discovered with status: 0
I   18:29:07.703    Services discovered
V   18:29:07.707    Generic Access (0x1800)
- Device Name [R W] (0x2A00)
- Appearance [R] (0x2A01)
- Peripheral Preferred Connection Parameters [R] (0x2A04)
- Central Address Resolution [R] (0x2AA6)
Generic Attribute (0x1801)
- Service Changed [I] (0x2A05)
   Client Characteristic Configuration (0x2902)
Nordic UART Service (6e400001-b5a3-f393-e0a9-e50e24dcca9e)
- RX Characteristic [W WNR] (6e400002-b5a3-f393-e0a9-e50e24dcca9e)
- TX Characteristic [N] (6e400003-b5a3-f393-e0a9-e50e24dcca9e)
   Client Characteristic Configuration (0x2902)
Secure DFU Service (0xFE59)
- Buttonless DFU [I W] (8ec90003-f315-4f60-9fb8-838830daea50)
   Client Characteristic Configuration (0x2902)
D   18:29:07.708    gatt.setCharacteristicNotification(00002a05-0000-1000-8000-00805f9b34fb, true)
D   18:29:07.711    gatt.setCharacteristicNotification(6e400003-b5a3-f393-e0a9-e50e24dcca9e, true)
I   18:29:07.763    Connection parameters updated (interval: 50.0ms, latency: 0, timeout: 5000ms)
kevincar commented 2 years ago

Thanks for the clarification. I think I understand your situation. So that we can pin point whether the issue directly associated with bless and not another piece of your workflow, can you post the following:

  1. nRF connect logs when connecting to the Raspberry Pi running the example/server.py
  2. Confirm that the same behavior occurs when using LightBlue app
arist0v commented 2 years ago

The log have already been posted, it was when using my code, but they are the same.

I will try with your app

Le jeu. 10 nov. 2022, 19 h 41, Kevin Davis @.***> a écrit :

Thanks for the clarification. I think I understand your situation. So that we can pin point whether the issue directly associated with bless and not another piece of your workflow, can you post the following:

  1. nRF connect logs when connecting to the Raspberry Pi running the example/server.py
  2. Confirm that the same behavior occurs Im when using LightBlue app

— Reply to this email directly, view it on GitHub https://github.com/kevincar/bless/issues/87#issuecomment-1311085409, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABSPAABHIT3N5CZOT3SLLP3WHWI3HANCNFSM53VQDGVA . You are receiving this because you authored the thread.Message ID: @.***>

arist0v commented 2 years ago

I can confirm the same issue with lightblue

Le jeu. 10 nov. 2022, 20 h 09, Martin Verret @.***> a écrit :

The log have already been posted, it was when using my code, but they are the same.

I will try with your app

Le jeu. 10 nov. 2022, 19 h 41, Kevin Davis @.***> a écrit :

Thanks for the clarification. I think I understand your situation. So that we can pin point whether the issue directly associated with bless and not another piece of your workflow, can you post the following:

  1. nRF connect logs when connecting to the Raspberry Pi running the example/server.py
  2. Confirm that the same behavior occurs Im when using LightBlue app

— Reply to this email directly, view it on GitHub https://github.com/kevincar/bless/issues/87#issuecomment-1311085409, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABSPAABHIT3N5CZOT3SLLP3WHWI3HANCNFSM53VQDGVA . You are receiving this because you authored the thread.Message ID: @.***>

kevincar commented 2 years ago

Thanks for the update.

I'll see if I can whip up my Raspberry Pi to run Raspian. I've currently tested bless on Raspberry Pi running Ubuntu and it works properly, but haven't checked Raspian. I'll keep you posted

kevincar commented 2 years ago

In the mean time, do you have any way to confirm this isn't an android issue? See here

arist0v commented 2 years ago

I have no issue connecting to the nordic board, so android is not the issue since the same app try to connect to 2 different device (raspberry pi and nordic board) that (should) publish the same gatt service

h0bb3 commented 1 year ago

I can confirm this problem on RPI 4 (python:3.8 based docker container managed by balena)

Usingasyncio.Event solved the problem

I would at least suggest a comment in the example file regarding this possible issue.

kevincar commented 1 year ago

Thanks for this confirmation @tobias-dv-lnu. I've pushed an update on a development branch to include your suggestion. See here

However, I'm not sure this solves @arist0v 's issue on Raspian though. To confirm, did the switch to using an asyncio.Event work for you while using Ubuntu on RPI 4, or are you using Raspian?

arist0v commented 1 year ago

Our project who use this is on hold for i don't know how many time, so i won't be able to confiirm anything soon, if you really need confirmation from me please tell me and i will setup stuff to try to help as much as i can

kevincar commented 1 year ago

@arist0v sounds good. No need for now but I’ll let you know. I still need to confirm for myself first. Thanks!

h0bb3 commented 1 year ago

@kevincar nice!

Our docker container is FROM: python:3.11 -> i guess this is debian based in the bottom of things.

FROM python:3.11
RUN apt-get update
RUN apt-get install -y build-essential bluetooth libdbus-1-dev libgirepository1.0-dev libudev-dev libical-dev
ENV DBUS_SYSTEM_BUS_ADDRESS=unix:path=/host/run/dbus/system_bus_socket
WORKDIR /code
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
RUN pip install dbus-python pycairo PyGObject
RUN pip install python-networkmanager
COPY . .
ENTRYPOINT sh docker_entrypoint.sh

docker_entrypoint.sh:

#!/bin/bash

service dbus start
bluetoothd &

python ble_service.py

/bin/bash
kevincar commented 1 year ago

To follow up. I tested the updated server.py example file on Raspbian v6.1.21 released on 2023-5-3 and it worked without issues. I test this using an iOS device as a central and first ensured that the devices were paired prior to testing.

I'm going to close this since it is working as expected. If anyone is still having issues with this please reopen and we'll continue the discussion.