195 lines
7.0 KiB
Python
195 lines
7.0 KiB
Python
try:
|
|
import bluetooth
|
|
has_ble = True
|
|
except ImportError:
|
|
has_ble = False
|
|
|
|
import struct
|
|
import time
|
|
|
|
if not has_ble:
|
|
class BLEUART:
|
|
def __init__(self, ble=None, name="ESP32-S3-RLCD"):
|
|
print("BLE not supported on this platform.")
|
|
def on_rx(self, callback): pass
|
|
def write(self, data): return False
|
|
def is_connected(self): return False
|
|
def scan(self, duration_ms=3000): return {}
|
|
def close(self): pass
|
|
else:
|
|
# BLE UUID definitions (Nordic UART Service)
|
|
_UART_UUID = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
|
|
_UART_TX = (
|
|
bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"),
|
|
bluetooth.FLAG_NOTIFY,
|
|
)
|
|
_UART_RX = (
|
|
bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"),
|
|
bluetooth.FLAG_WRITE | bluetooth.FLAG_WRITE_NO_RESPONSE,
|
|
)
|
|
_UART_SERVICE = (_UART_UUID, (_UART_TX, _UART_RX))
|
|
|
|
# BLE IRQ constants
|
|
_IRQ_CENTRAL_CONNECT = 1
|
|
_IRQ_CENTRAL_DISCONNECT = 2
|
|
_IRQ_GATTS_WRITE = 3
|
|
|
|
class _ActiveBLEUART:
|
|
"""A helper class to manage BLE UART (Serial Over BLE) for raw text data exchange."""
|
|
|
|
def __init__(self, ble=None, name="ESP32-S3-RLCD"):
|
|
self._ble = ble if ble is not None else bluetooth.BLE()
|
|
self._ble.active(True)
|
|
self._ble.irq(self._irq)
|
|
|
|
# Register UART service
|
|
((self._tx_handle, self._rx_handle),) = self._ble.gatts_register_services((_UART_SERVICE,))
|
|
|
|
self._connections = set()
|
|
self._rx_callback = None
|
|
self._name = name
|
|
self._scan_results = {}
|
|
|
|
self._advertise()
|
|
|
|
def _irq(self, event, data):
|
|
# Handle connection, disconnection, and writes
|
|
if event == _IRQ_CENTRAL_CONNECT:
|
|
conn_handle, _, _ = data
|
|
self._connections.add(conn_handle)
|
|
print(f"BLE Central connected (handle: {conn_handle})")
|
|
|
|
elif event == _IRQ_CENTRAL_DISCONNECT:
|
|
conn_handle, _, _ = data
|
|
if conn_handle in self._connections:
|
|
self._connections.remove(conn_handle)
|
|
print(f"BLE Central disconnected (handle: {conn_handle})")
|
|
# Restart advertising
|
|
self._advertise()
|
|
|
|
elif event == _IRQ_GATTS_WRITE:
|
|
conn_handle, value_handle = data
|
|
if value_handle == self._rx_handle:
|
|
# Read received data
|
|
data_received = self._ble.gatts_read(self._rx_handle)
|
|
if self._rx_callback:
|
|
try:
|
|
decoded = data_received.decode("utf-8").strip()
|
|
self._rx_callback(decoded)
|
|
except Exception as e:
|
|
# Fallback for binary / non-UTF-8 data
|
|
self._rx_callback(data_received)
|
|
|
|
elif event == 5: # _IRQ_GAP_SCAN_RESULT
|
|
addr_type, addr, adv_type, rssi, adv_data = data
|
|
# Convert address to string hex format
|
|
mac = ':'.join(f'{b:02x}' for b in addr)
|
|
name = ""
|
|
try:
|
|
# Parse advertising payload to extract complete (0x09) or short (0x08) name
|
|
i = 0
|
|
while i < len(adv_data):
|
|
length = adv_data[i]
|
|
if length == 0 or i + length + 1 > len(adv_data):
|
|
break
|
|
ad_type = adv_data[i+1]
|
|
if ad_type in (0x08, 0x09):
|
|
name = adv_data[i+2 : i+1+length].decode('utf-8')
|
|
break
|
|
i += length + 1
|
|
except:
|
|
pass
|
|
self._scan_results[mac] = {"rssi": rssi, "name": name}
|
|
|
|
def _advertise(self):
|
|
# Generate advertising payload (flags + service UUID = 3 + 18 = 21 bytes)
|
|
adv_data = self._build_advertising_payload(services=[_UART_UUID], include_flags=True)
|
|
# Generate scan response payload (device name, no flags = 2 + len(name) bytes)
|
|
resp_data = self._build_advertising_payload(name=self._name, include_flags=False)
|
|
|
|
self._ble.gap_advertise(100000, adv_data=adv_data, resp_data=resp_data)
|
|
print(f"BLE advertising started as '{self._name}'")
|
|
|
|
def _build_advertising_payload(self, name=None, services=None, include_flags=True):
|
|
"""Generates standard BLE advertising packet format."""
|
|
payload = bytearray()
|
|
|
|
def append(ad_type, value):
|
|
payload.append(len(value) + 1)
|
|
payload.append(ad_type)
|
|
payload.extend(value)
|
|
|
|
# Flags: General discoverable mode, BR/EDR not supported
|
|
if include_flags:
|
|
append(0x01, struct.pack("B", 0x02 | 0x04))
|
|
|
|
# Name
|
|
if name:
|
|
append(0x09, name.encode("utf-8"))
|
|
|
|
# Services UUID
|
|
if services:
|
|
for uuid in services:
|
|
# UUID bytes are in little-endian order
|
|
append(0x07, bytes(uuid))
|
|
|
|
return bytes(payload)
|
|
|
|
def on_rx(self, callback):
|
|
"""Register a callback function to handle incoming BLE messages.
|
|
|
|
Args:
|
|
callback (function): Function accepting a single string argument.
|
|
"""
|
|
self._rx_callback = callback
|
|
return callback
|
|
|
|
def write(self, data):
|
|
"""Sends data (string or bytes) to all connected BLE Centrals."""
|
|
if not self.is_connected():
|
|
return False
|
|
|
|
# Ensure it is bytes
|
|
if isinstance(data, str):
|
|
data = data.encode("utf-8")
|
|
|
|
try:
|
|
# Write value to local GATT DB first
|
|
self._ble.gatts_write(self._tx_handle, data)
|
|
# Notify all connected clients
|
|
for conn_handle in self._connections:
|
|
self._ble.gatts_notify(conn_handle, self._tx_handle, data)
|
|
return True
|
|
except Exception as e:
|
|
print(f"Error sending BLE data: {e}")
|
|
return False
|
|
|
|
def is_connected(self):
|
|
"""Returns True if at least one central is connected."""
|
|
return len(self._connections) > 0
|
|
|
|
def scan(self, duration_ms=3000):
|
|
"""Scans for nearby BLE devices and returns a dictionary of results.
|
|
|
|
Returns:
|
|
dict: { 'mac_address': { 'rssi': -70, 'name': 'DeviceName' } }
|
|
"""
|
|
self._scan_results = {}
|
|
# Start passive scan (active=True requests scan responses to get names)
|
|
# Scan parameters: duration, interval (20ms), window (10ms), active (True)
|
|
self._ble.gap_scan(duration_ms, 20000, 10000, True)
|
|
time.sleep_ms(duration_ms + 100)
|
|
self._ble.gap_scan(None) # Ensure scan stops
|
|
return self._scan_results
|
|
|
|
def close(self):
|
|
"""Disconnect and stop BLE."""
|
|
self._ble.gap_advertise(None) # Stop advertising
|
|
for conn_handle in self._connections:
|
|
self._ble.gap_disconnect(conn_handle)
|
|
self._ble.active(False)
|
|
print("BLE closed.")
|
|
|
|
if has_ble:
|
|
BLEUART = _ActiveBLEUART
|