aioble
======

This library provides an object-oriented, asyncio-based wrapper for MicroPython's
[bluetooth](https://docs.micropython.org/en/latest/library/bluetooth.html) API.

**Note**: aioble requires MicroPython v1.17 or higher.

Features
--------

Broadcaster (advertiser) role:
* Generate advertising and scan response payloads for common fields.
* Automatically split payload over advertising and scan response.
* Start advertising (indefinitely or for duration).

Peripheral role:
* Wait for connection from central.
* Wait for MTU exchange.

Observer (scanner) role:
* Scan for devices (passive + active).
* Combine advertising and scan response payloads for the same device.
* Parse common fields from advertising payloads.

Central role:
* Connect to peripheral.
* Initiate MTU exchange.

GATT Client:
* Discover services, characteristics, and descriptors (optionally by UUID).
* Read / write / write-with-response characters and descriptors.
* Subscribe to notifications and indications on characteristics (via the CCCD).
* Wait for notifications and indications.

GATT Server:
* Register services, characteristics, and descriptors.
* Wait for writes on characteristics and descriptors.
* Intercept read requests.
* Send notifications and indications (and wait on response).

L2CAP:
* Accept and connect L2CAP Connection-oriented-channels.
* Manage channel flow control.

Security:
* JSON-backed key/secret management.
* Initiate pairing.
* Query encryption/authentication state.

All remote operations (connect, disconnect, client read/write, server indicate, l2cap recv/send, pair) are awaitable and support timeouts.

Installation
------------

You can install any combination of the following packages.
- `aioble-central` -- Central (and Observer) role functionality including
  scanning and connecting.
- `aioble-client` -- GATT client, typically used by central role devices but
  can also be used on peripherals.
- `aioble-l2cap` -- L2CAP Connection-oriented-channels support.
- `aioble-peripheral` -- Peripheral (and Broadcaster) role functionality
  including advertising.
- `aioble-security` -- Pairing and bonding support.
- `aioble-server` -- GATT server, typically used by peripheral role devices
  but can also be used on centrals.

Alternatively, install the `aioble` package, which will install everything.

Usage
-----

#### Passive scan for nearby devices for 5 seconds: (Observer)

```py
async with aioble.scan(duration_ms=5000) as scanner:
    async for result in scanner:
        print(result, result.name(), result.rssi, result.services())
```

Active scan (includes "scan response" data) for nearby devices for 5 seconds
with the highest duty cycle: (Observer)

```py
async with aioble.scan(duration_ms=5000, interval_us=30000, window_us=30000, active=True) as scanner:
    async for result in scanner:
        print(result, result.name(), result.rssi, result.services())
```

#### Connect to a peripheral device: (Central)

```py
# Either from scan result
device = result.device
# Or with known address
device = aioble.Device(aioble.ADDR_PUBLIC, "aa:bb:cc:dd:ee:ff")

try:
    connection = await device.connect(timeout_ms=2000)
except asyncio.TimeoutError:
    print('Timeout')
```

#### Register services and wait for connection: (Peripheral, Server)

```py
_ENV_SENSE_UUID = bluetooth.UUID(0x181A)
_ENV_SENSE_TEMP_UUID = bluetooth.UUID(0x2A6E)
_GENERIC_THERMOMETER = const(768)

_ADV_INTERVAL_US = const(250000)

temp_service = aioble.Service(_ENV_SENSE_UUID)
temp_char = aioble.Characteristic(temp_service, _ENV_SENSE_TEMP_UUID, read=True, notify=True)

aioble.register_services(temp_service)

while True:
    connection = await aioble.advertise(
            _ADV_INTERVAL_US,
            name="temp-sense",
            services=[_ENV_SENSE_UUID],
            appearance=_GENERIC_THERMOMETER,
            manufacturer=(0xabcd, b"1234"),
        )
    print("Connection from", device)
```

#### Update characteristic value: (Server)

```py
# Write the local value.
temp_char.write(b'data')
```

```py
# Write the local value and notify/indicate subscribers.
temp_char.write(b'data', send_update=True)
```

#### Send notifications: (Server)

```py
# Notify with the current value.
temp_char.notify(connection)
```

```py
# Notify with a custom value.
temp_char.notify(connection, b'optional data')
```

#### Send indications: (Server)

```py
# Indicate with current value.
await temp_char.indicate(connection, timeout_ms=2000)
```

```py
# Indicate with custom value.
await temp_char.indicate(connection, b'optional data', timeout_ms=2000)
```

This will raise `GattError` if the indication is not acknowledged.

#### Wait for a write from the client: (Server)

```py
# Normal characteristic, returns the connection that did the write.
connection = await char.written(timeout_ms=2000)
```

```py
# Characteristic with capture enabled, also returns the value.
char = Characteristic(..., capture=True)
connection, data = await char.written(timeout_ms=2000)
```

#### Query the value of a characteristic: (Client)

```py
temp_service = await connection.service(_ENV_SENSE_UUID)
temp_char = await temp_service.characteristic(_ENV_SENSE_TEMP_UUID)

data = await temp_char.read(timeout_ms=1000)
```

#### Wait for a notification/indication: (Client)

```py
# Notification
data = await temp_char.notified(timeout_ms=1000)
```

```py
# Indication
data = await temp_char.indicated(timeout_ms=1000)
```

#### Subscribe to a characteristic: (Client)

```py
# Subscribe for notification.
await temp_char.subscribe(notify=True)
while True:
    data = await temp_char.notified()
```

```py
# Subscribe for indication.
await temp_char.subscribe(indicate=True)
while True:
    data = await temp_char.indicated()
```

#### Open L2CAP channels: (Listener)

```py
channel = await connection.l2cap_accept(_L2CAP_PSN, _L2CAP_MTU)
buf = bytearray(64)
n = channel.recvinto(buf)
channel.send(b'response')
```

#### Open L2CAP channels: (Initiator)

```py
channel = await connection.l2cap_connect(_L2CAP_PSN, _L2CAP_MTU)
channel.send(b'request')
buf = bytearray(64)
n = channel.recvinto(buf)
```


Examples
--------

See the `examples` directory for some example applications.

* temp_sensor.py: Temperature sensor peripheral.
* temp_client.py: Connects to the temp sensor.
* l2cap_file_server.py: Simple file server peripheral. (WIP)
* l2cap_file_client.py: Client for the file server. (WIP)

Tests
-----

The `multitests` directory provides tests that can be run with MicroPython's `run-multitests.py` script. These are based on the existing `multi_bluetooth` tests that are in the main repo.
