Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nRF52: BLE Python Helper Classes #586

Closed
microbuilder opened this issue Feb 6, 2018 · 15 comments
Closed

nRF52: BLE Python Helper Classes #586

microbuilder opened this issue Feb 6, 2018 · 15 comments

Comments

@microbuilder
Copy link

microbuilder commented Feb 6, 2018

Easier to use helper classes on top of the current low level BLE library are required for the following:

Peripheral Mode

Core Classes

  • GAP: Advertising
  • GATT: Characteristic Wrapper
  • GATT: Service Wrapper
  • GATT: Description Wrappers (CCCD, etc.)

Service Wrappers

  • Device Information Service
  • BLE UART (NUS)
  • Physical Web (formerly Eddystone)

Central Mode

TBD

Requirements

These helpers classes should require no packet level knowledge from users, which is currently the case constructing advertising packets, etc.

The wrappers must work with both S132 (nRF52832 boards) and S140 (nRF52840) SD APIs.

@microbuilder microbuilder self-assigned this Feb 6, 2018
@arturo182
Copy link
Collaborator

Are we talking about Python classes here? Are we following the bluepy (http://ianharvey.github.io/bluepy-doc/) API? I would like to help but need to decide on the API first.

@microbuilder microbuilder added this to the 3.0 milestone Feb 6, 2018
@arturo182
Copy link
Collaborator

In the future we could also look into adding HID and ANCS support, they are quite useful.

@microbuilder
Copy link
Author

Yeah, the goal is ideally something sitting on top of bluepy (for portability reasons), and I'm talking about Python helper classes, I'll clarify that in the subject line.

The current bluepy API assumes a reasonable amount of knowledge at the packet level and about BLE, and helper classes can make working with things like indicate/notify and advertising properly much less error prone for end users.

@microbuilder microbuilder changed the title nRF52: BLE Helper Classes nRF52: BLE Python Helper Classes Feb 6, 2018
@microbuilder
Copy link
Author

@arturo182 We already have helpers for a lot of useful services like ANCS for Arduino ... the goal is of course migrating similar helpers over to Python, though the API will obviously be different on a different platform and language: https://github.com/adafruit/Adafruit_nRF52_Arduino/tree/master/libraries/Bluefruit52Lib/src

@arturo182
Copy link
Collaborator

If we want to only support s132 and s140, does that mean we drop support for s110, s120 and s130? I see there are many defines for those SDs in the ble driver and I'm not sure how well those have been tested, does CP even run on those 16K/32K RAM nRF51 devices? Maybe we should remove the board and linker files for the nRF51 family and only support nRF52.

And another question, is the pca10040 supported? Because I couldn't build it, was missing a board.c file, how did you test the ble code when developing,w as it only on the feather52?

@arturo182
Copy link
Collaborator

I think we should also have an issue for more advanced BLE features like security, bonding (and we need somewhere to keep the IRK), advertising whitelists, and connection interval management (important for power efficiency).

@microbuilder
Copy link
Author

Personally, S132 and S140 are the only targets that I think have a lot of value moving forward. The older devices are too small to be useful. As part of the next PR, I was actually planning on removing a lot of boards, and focus on the micro:bit, feather52 and PCA10056 boards exclusively, or anything similar enough to those to easily maintain without stretching ourselves too thin. The combination of S132/S140 and nRF52832 (32+512KB) or nRF52840 are all that really make sense to me.

@arturo182
Copy link
Collaborator

That sounds good, If we remove the older SDs from the ble driver i think it will become way cleaner and since s132 and s140 SDK is fairly similar, there won't be that many ifdefs.

I think it's worth leaving the pca10040 in, but remove all the nRF51 boards.

Let me know how I can help, don't want to step on your toes.

@tannewt
Copy link
Member

tannewt commented Feb 6, 2018

It sounds like the bluepy API will work to build the higher level helpers on top of. The current ubluepy is slightly different from the proper bluepy API and it'd be great to redo ours to be an exact subset (or identical). The biggest difference I know of is that ubluepy doesn't use properties. That's a MicroPython design decision that we don't follow. (We pick consistency over memory optimization.)

Here is an example of a difference: bluepy vs ubluepy

Here is an example of a Python property in C.

It would be awesome to introduce a bluepy module to the shared-bindings directory and have it be consistent with CPython's bluepy. It would then call a C api that any port can implement to provide BLE.

@arturo182
Copy link
Collaborator

For me right now the blocker on this issue (in addition to some time limitations) is agreeing on a API so we can continue BLE development. I know we want a API that would easily port to RPi and similar.

So to continue the conversation, I recently found a new library that has some potential, https://github.com/TheCellule/python-bleson

It aims to work on Linux, Mac and Windows, there is some mention of them working on a MicroPython port but not sure what's the status on that, TheCellule/python-bleson@cc5d4f8 is best I could find

It seems there has been no work for 2 months but I think it offers a good starting point if we wanted to either help them make it work on RPi or fork it.

I really like their example code, even though it does not work (classes are not implemented yet), the idea they present is very nice, for example:
https://github.com/TheCellule/python-bleson/blob/master/examples/basic_advertiser_heartrate.py

https://github.com/TheCellule/python-bleson/blob/master/docs/examples.rst#peripheral-example (I really like this API idea, can't get any simpler)

Thoughts?

@tannewt
Copy link
Member

tannewt commented Apr 13, 2018

I'm a little wary of using callbacks because it is a form of concurrency. Overall I like the API though.

In general, lets not get too hung up on the API. We can evolve it as we learn what we need. Remember we can potentially have two or more APIs: 1) a low level API that is the minimum the C needs to expose and 2) a user-friendly API (or more) that are for specific use cases.

@arturo182
Copy link
Collaborator

Ok then, I will try to get something going then and we'll see how it will end up looking :)

@tannewt
Copy link
Member

tannewt commented Apr 13, 2018 via email

@tannewt
Copy link
Member

tannewt commented Jul 18, 2018

Ok, here is a sketch of what we could do. The idea is that the same object is used on the client and server. It runs in CPython as-is (actual ble is faked).

import collections
import struct

# in C

# use this to instantiate unknown devices with known services
all_services = {}

def register_service(cls):
    all_services[cls.uuid] = (cls.field_name, cls)
    return cls

# use this to instantiate known devices
all_devices = {}

def register_device(cls):
    all_services[cls.pnp_id] = cls
    return cls

# in C
class UUID:
    def __init__(self, value):
        self._value = value


class Characteristic:
    pass

class StaticCharacteristic(Characteristic):
    def __init__(self, starting_data=None):
        self._data = starting_data

    @property
    def raw_data(self):
        return self._data

    @raw_data.setter
    def raw_data(self, value):
        self._data = value

# if notify or indicate is possible then act like a queue
class DynamicCharacteristic(Characteristic):
    def __init__(self, *, indicate=False):
        self._indicate = indicate
        self._enabled = False

    def get(self):
        return None

    def put(self, value):
        # internal stuff
        pass

    @property
    def enabled(self):
        return self._enabled

    def enabled(self, value):
        self._enabled = value

# in C
class Service:
    def __init__(self, device):
        self._device = device

class Device:
    def __init__(self, id=None):
        self.id = id
        self.peripheral = id == None

    @property
    def connected(self):
        # c state
        return True

# in python lib
class Uint8Characteristic(StaticCharacteristic):
    def __init__(self, *, min_value=0, max_value=255):
        self._min_value = min_value
        self._max_value = max_value

    def __get__(self, obj, type=None):
        # unpack from the wire format
        print("get", self)
        return self._max_value

    def __set__(self, obj, value):
        if not self._min_value <= value <= self._max_value:
            raise ValueError("out of range")
        # pack to the wire format
        print("set", self)

class StaticStruct(StaticCharacteristic):
    def __init__(self, value_type, struct_format):
        super().__init__()
        self._value_type = value_type
        self._struct_format = struct_format

    def __get__(self, obj, type=None):
        # unpack from the wire format
        print("get", self)
        return self._value_type._make(struct.unpack(self._struct_format, self.raw_data))

    def __set__(self, obj, value):
        # pack to the wire format
        print("set", self)
        self.raw_data = struct.pack(self._struct_format, *value)

PnPId = collections.namedtuple("PnPId", ("vendor_id_source", "vendor_id", "product_id", "product_version"))

class PnPIdCharacteristic(StaticStruct):
    def __init__(self):
        super().__init__(PnPId, "BHHH")

    def __set__(self, obj, value):
        if not 1 <= value.vendor_id_source <= 2:
            raise ValueError("out of range")
        super().__set__(obj, value)

# in python lib
class BatteryLevel(Uint8Characteristic):
    uuid = UUID(0x2A19)
    def __init__(self):
        super().__init__(min_value=0, max_value=100)

# in python lib
@register_service
class BatteryService(Service):
    uuid = UUID(0x180f)
    field_name = "battery"
    level = BatteryLevel()

@register_service
class DeviceInformation(Service):
    uuid = UUID(0x180a)
    field_name = "device"
    pnp_id = PnPIdCharacteristic()


# in python user code
@register_device
class MyDevice(Device):
    pnp_id=PnPId(2, 0x1234, 0x4567, 1)
    def __init__(self, id=None):
        super().__init__(id)
        self.battery = BatteryService(self)
        self.device = DeviceInformation(self)

        # set our default state
        if self.peripheral:
            self.device.pnp_id=MyDevice.pnp_id

print(all_services)

d = MyDevice()
print(d.battery.level)
d.battery.level = 90

@tannewt tannewt modified the milestones: 4.0 Beta, 4.0.0 - Bluetooth Dec 7, 2018
@tannewt
Copy link
Member

tannewt commented Feb 5, 2019

@dhalbert Has done a bunch of work on this with these libraries:

More will come but this is good for now.

@tannewt tannewt closed this as completed Feb 5, 2019
@tannewt tannewt mentioned this issue Oct 22, 2019
tannewt added a commit to tannewt/Adafruit_CircuitPython_BLE that referenced this issue Oct 23, 2019
This makes Advertisement and Service definitions declarative by
factoring out parsing logic out into shareable descriptor classes
similar to how the Register library works.

This also introduces SmartAdapter and SmartConnection which will
auto-create the correct Advertisements and Services without requiring
any direct use of UUIDs. Instead, classes are used to identify
relevant objects to "recognize".

This requires adafruit/circuitpython#2236 and
relates to adafruit/circuitpython#586.
dhalbert pushed a commit that referenced this issue Nov 5, 2019
This PR refines the _bleio API. It was originally motivated by
the addition of a new CircuitPython service that enables reading
and modifying files on the device. Moving the BLE lifecycle outside
of the VM motivated a number of changes to remove heap allocations
in some APIs.

It also motivated unifying connection initiation to the Adapter class
rather than the Central and Peripheral classes which have been removed.
Adapter now handles the GAP portion of BLE including advertising, which
has moved but is largely unchanged, and scanning, which has been enhanced
to return an iterator of filtered results.

Once a connection is created (either by us (aka Central) or a remote
device (aka Peripheral)) it is represented by a new Connection class.
This class knows the current connection state and can discover and
instantiate remote Services along with their Characteristics and
Descriptors.

Relates to #586
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants