You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
108 lines
3.4 KiB
108 lines
3.4 KiB
"""
|
|
# Select a BLE radio on the PC for for use by bluetoothctl (apparently it can only use one at a time).
|
|
sudo btmgmt info
|
|
bluetoothctl select 11:22:33:44:55:66
|
|
|
|
# Readies the device for pairing.
|
|
bluetoothctl connect 77:88:99:AA:BB:CC
|
|
|
|
# Actually pairs. Will prompt for pin.
|
|
bluetoothctl pair 77:88:99:AA:BB:CC
|
|
|
|
# Sets the device to re-pair automatically when it is turned on, which eliminates the need to pair all over again.
|
|
bluetoothctl trust 77:88:99:AA:BB:CC
|
|
|
|
# Check whether paired and connected.
|
|
bluetoothctl devices
|
|
bluetoothctl info 77:88:99:AA:BB:CC
|
|
|
|
# Disconnect bluez from the device. Sometimes bleak needs this to own the connection.
|
|
bluetoothctl disconnect 77:88:99:AA:BB:CC
|
|
"""
|
|
|
|
|
|
import asyncio
|
|
from bleak import BleakClient
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Meshtastic BLE UUIDs
|
|
TO_RADIO_UUID = "f75c76d2-129e-4dad-a1dd-7866124401e7"
|
|
FROM_RADIO_UUID = "2c55e69e-4993-11ed-b878-0242ac120002"
|
|
FROM_NUM_UUID = "ed9da18c-a800-4f66-a670-aa7547e34453"
|
|
|
|
|
|
class BLETransport:
|
|
def __init__(self, address, adapter=None):
|
|
self.address = address
|
|
self.adapter = adapter
|
|
self.client = None
|
|
self._in_q = asyncio.Queue()
|
|
self._drain_lock = asyncio.Lock()
|
|
self._error = None
|
|
|
|
async def _drain(self):
|
|
if self.client is None:
|
|
return
|
|
async with self._drain_lock:
|
|
try:
|
|
while True:
|
|
data = await self.client.read_gatt_char(FROM_RADIO_UUID)
|
|
if not data:
|
|
break
|
|
await self._in_q.put(data)
|
|
except Exception as e:
|
|
self._error = e
|
|
logger.error("ble drain error: %s", e)
|
|
try:
|
|
self._in_q.put_nowait(None)
|
|
except Exception:
|
|
pass
|
|
|
|
async def start(self):
|
|
# Bleak expects the address as the first positional arg
|
|
# Meshtastic devices typically use random address type on BLE.
|
|
# Reset state to avoid stale sentinels/data from prior sessions
|
|
self._in_q = asyncio.Queue()
|
|
self._error = None
|
|
client = BleakClient(self.address, adapter=self.adapter, address_type="random")
|
|
await client.connect()
|
|
self.client = client
|
|
logger.debug("BLETransport.start: connected to %s (adapter=%s)", self.address, self.adapter)
|
|
|
|
def from_num_handler(_sender, _data):
|
|
asyncio.create_task(self._drain())
|
|
|
|
await client.start_notify(FROM_NUM_UUID, from_num_handler)
|
|
|
|
async def send(self, payload):
|
|
if self.client is None:
|
|
return
|
|
try:
|
|
await self.client.write_gatt_char(TO_RADIO_UUID, payload, response=True)
|
|
asyncio.create_task(self._drain())
|
|
except Exception as e:
|
|
self._error = e
|
|
logger.error("ble send error: %s", e)
|
|
try:
|
|
self._in_q.put_nowait(None)
|
|
except Exception:
|
|
pass
|
|
|
|
async def recv(self):
|
|
item = await self._in_q.get()
|
|
if item is None:
|
|
raise ConnectionError(self._error or "ble transport error")
|
|
return item
|
|
|
|
async def close(self):
|
|
if self.client is None:
|
|
return
|
|
try:
|
|
try:
|
|
await self.client.stop_notify(FROM_NUM_UUID)
|
|
except Exception:
|
|
pass
|
|
await self.client.disconnect()
|
|
finally:
|
|
self.client = None
|