Skip to content

Commit fd090c2

Browse files
Merge pull request #72 from jedie/ble-scan
Enhance ScanBluetooth app
2 parents eb1d7fb + 896f74d commit fd090c2

2 files changed

Lines changed: 106 additions & 84 deletions

File tree

internal_filesystem/apps/com.micropythonos.scan_bluetooth/META-INF/MANIFEST.JSON

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.scan_bluetooth/icons/com.micropythonos.scan_bluetooth_0.0.1_64x64.png",
77
"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.scan_bluetooth/mpks/com.micropythonos.scan_bluetooth_0.0.1.mpk",
88
"fullname": "com.micropythonos.scan_bluetooth",
9-
"version": "0.0.1",
9+
"version": "0.1.0",
1010
"category": "development",
1111
"activities": [
1212
{

internal_filesystem/apps/com.micropythonos.scan_bluetooth/assets/scan_bluetooth.py

Lines changed: 105 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -3,68 +3,45 @@
33
https://docs.micropython.org/en/latest/library/bluetooth.html
44
"""
55

6-
import time
7-
86
try:
97
import bluetooth
108
except ImportError: # Linux test runner may not provide bluetooth module
119
bluetooth = None
1210

11+
import sys
12+
1313
import lvgl as lv
1414
from micropython import const
15-
from mpos import Activity
15+
from mpos import Activity, TaskManager
1616

17-
SCAN_DURATION = const(1000) # Duration of each BLE scan in milliseconds
18-
_IRQ_SCAN_RESULT = const(5)
17+
# Scan for 5 seconds,
18+
SCAN_DURATION_MS = const(5000) # Duration of each BLE scan in milliseconds
19+
# with very low interval/window (to maximize detection rate):
20+
INTERVAL_US = const(30000)
21+
WINDOW_US = const(30000)
1922

23+
_IRQ_SCAN_RESULT = const(5)
24+
_IRQ_SCAN_DONE = const(6)
2025

2126
# BLE Advertising Data Types (Standardized by Bluetooth SIG)
22-
_ADV_TYPE_NAME = const(0x09)
27+
_ADV_TYPE_SHORT_NAME = const(8)
28+
_ADV_TYPE_NAME = const(9)
2329

2430

25-
def decode_field(payload: bytes, adv_type: int) -> list:
26-
results = []
31+
def decode_name(payload: bytes) -> str | None:
2732
i = 0
2833
payload_len = len(payload)
2934
while i < payload_len:
3035
length = payload[i]
3136
if length == 0 or i + length >= payload_len:
3237
break
3338
field_type = payload[i + 1]
34-
if field_type == adv_type:
35-
results.append(payload[i + 2 : i + length + 1])
39+
if field_type in (_ADV_TYPE_SHORT_NAME, _ADV_TYPE_NAME):
40+
if new_name := payload[i + 2 : i + length + 1]:
41+
return str(new_name, "utf-8")
42+
else:
43+
print(f"Unsupported: {field_type=} with {length=}")
3644
i += length + 1
37-
return results
38-
39-
40-
class BluetoothScanner:
41-
def __init__(self, device_callback):
42-
if bluetooth is None:
43-
raise RuntimeError("Bluetooth module not available")
44-
self.device_callback = device_callback
45-
self.ble = bluetooth.BLE()
46-
self.ble.irq(self.ble_irq_handler)
47-
48-
def __enter__(self):
49-
print("Activating BLE")
50-
self.ble.active(True)
51-
return self
52-
53-
def ble_irq_handler(self, event: int, data: tuple) -> None:
54-
if event == _IRQ_SCAN_RESULT:
55-
addr_type, addr, adv_type, rssi, adv_data = data
56-
addr = ":".join(f"{b:02x}" for b in addr)
57-
names = decode_field(adv_data, _ADV_TYPE_NAME)
58-
name = str(names[0], "utf-8") if names else "Unknown"
59-
self.device_callback(addr, rssi, name)
60-
61-
def scan(self, duration_ms: int):
62-
print(f"BLE scanning for {duration_ms}ms...")
63-
self.ble.gap_scan(duration_ms, 20000, 10000)
64-
65-
def __exit__(self, exc_type, exc_val, exc_tb):
66-
print("Deactivating BLE")
67-
self.ble.active(False)
6845

6946

7047
def set_dynamic_column_widths(table, font=None, padding=8):
@@ -85,75 +62,120 @@ def set_cell_value(table, *, row: int, values: tuple):
8562

8663

8764
class ScanBluetooth(Activity):
88-
refresh_timer = None
89-
9065
def onCreate(self):
91-
screen = lv.obj()
92-
screen.set_flex_flow(lv.FLEX_FLOW.COLUMN)
93-
screen.set_style_pad_all(0, 0)
94-
screen.set_size(lv.pct(100), lv.pct(100))
66+
main_content = lv.obj()
67+
main_content.set_flex_flow(lv.FLEX_FLOW.COLUMN)
68+
main_content.set_style_pad_all(0, 0)
69+
main_content.set_size(lv.pct(100), lv.pct(100))
70+
71+
info_column = lv.obj(main_content)
72+
info_column.set_flex_flow(lv.FLEX_FLOW.COLUMN)
73+
info_column.set_style_pad_all(1, 1)
74+
info_column.set_size(lv.pct(100), lv.SIZE_CONTENT)
75+
76+
self.info_label = lv.label(info_column)
77+
self.info_label.set_style_text_font(lv.font_montserrat_14, 0)
9578

9679
if bluetooth is None:
97-
label = lv.label(screen)
98-
label.set_text("Bluetooth not available on this platform")
99-
label.center()
100-
self.setContentView(screen)
80+
self.info("Bluetooth not available on this platform")
81+
self.setContentView(main_content)
10182
return
10283

103-
self.table = lv.table(screen)
84+
tabel_column = lv.obj(main_content)
85+
tabel_column.set_flex_flow(lv.FLEX_FLOW.COLUMN)
86+
tabel_column.set_style_pad_all(0, 0)
87+
tabel_column.set_size(lv.pct(100), lv.SIZE_CONTENT)
88+
89+
self.table = lv.table(tabel_column)
10490
set_cell_value(
10591
self.table,
10692
row=0,
10793
values=("pos", "MAC", "RSSI", "count", "Name"),
10894
)
10995
set_dynamic_column_widths(self.table)
11096

97+
self.scan_count = 0
11198
self.mac2column = {}
11299
self.mac2counts = {}
100+
self.mac2name = {}
113101

114-
self.scanner_cm = BluetoothScanner(device_callback=self.scan_callback)
115-
self.scanner = self.scanner_cm.__enter__() # Activate BLE
102+
self.ble = bluetooth.BLE()
116103

117-
self.setContentView(screen)
104+
self.setContentView(main_content)
118105

119-
def scan_callback(self, addr, rssi, name):
120-
if not (column_index := self.mac2column.get(addr)):
121-
column_index = len(self.mac2column) + 1
122-
self.mac2column[addr] = column_index
123-
self.mac2counts[addr] = 1
124-
else:
125-
self.mac2counts[addr] += 1
106+
def info(self, text):
107+
print(text)
108+
self.info_label.set_text(text)
126109

127-
set_cell_value(
128-
self.table,
129-
row=column_index,
130-
values=(
131-
str(column_index),
132-
addr,
133-
f"{rssi} dBm",
134-
str(self.mac2counts[addr]),
135-
name,
136-
),
137-
)
110+
async def ble_scan(self):
111+
"""Check sensor every second"""
112+
while self.scanning:
113+
print(f"async scan for {SCAN_DURATION_MS}ms...")
114+
self.ble.gap_scan(SCAN_DURATION_MS, INTERVAL_US, WINDOW_US, True)
115+
await TaskManager.sleep_ms(SCAN_DURATION_MS + 100)
138116

139117
def onResume(self, screen):
140118
super().onResume(screen)
141119
if bluetooth is None:
142120
return
143121

144-
def update(timer):
145-
self.scanner.scan(SCAN_DURATION)
146-
set_dynamic_column_widths(self.table)
147-
time.sleep_ms(SCAN_DURATION + 100) # Wait ?
148-
print(f"Scan complete: {len(self.mac2column)} unique devices")
122+
self.info("Activating Bluetooth...")
123+
self.ble.irq(self.ble_irq_handler)
124+
self.ble.active(True)
149125

150-
self.refresh_timer = lv.timer_create(update, SCAN_DURATION + 1000, None)
126+
self.scanning = True
127+
TaskManager.create_task(self.ble_scan())
151128

152129
def onPause(self, screen):
153130
super().onPause(screen)
154131
if bluetooth is None:
155132
return
156-
self.scanner.__exit__(None, None, None) # Deactivate BLE
157-
if self.refresh_timer:
158-
self.refresh_timer.delete()
159-
self.refresh_timer = None
133+
134+
self.scanning = False
135+
136+
self.info("Stop scanning...")
137+
self.ble.gap_scan(None)
138+
self.info("Deactivating BLE...")
139+
self.ble.active(False)
140+
self.info("BLE deactivated")
141+
142+
def ble_irq_handler(self, event: int, data: tuple) -> None:
143+
try:
144+
if event == _IRQ_SCAN_RESULT:
145+
addr_type, addr, adv_type, rssi, adv_data = data
146+
addr = ":".join(f"{b:02x}" for b in addr)
147+
print(f"{addr=} {rssi=} {len(adv_data)=}")
148+
if name := decode_name(adv_data):
149+
self.mac2name[addr] = name
150+
else:
151+
name = self.mac2name.get(addr, "Unknown")
152+
153+
if not (column_index := self.mac2column.get(addr)):
154+
column_index = len(self.mac2column) + 1
155+
self.mac2column[addr] = column_index
156+
self.mac2counts[addr] = 1
157+
else:
158+
self.mac2counts[addr] += 1
159+
160+
set_cell_value(
161+
self.table,
162+
row=column_index,
163+
values=(
164+
str(column_index),
165+
addr,
166+
f"{rssi} dBm",
167+
str(self.mac2counts[addr]),
168+
name,
169+
),
170+
)
171+
elif event == _IRQ_SCAN_DONE:
172+
set_dynamic_column_widths(self.table)
173+
self.scan_count += 1
174+
self.info(
175+
f"{len(self.mac2column)} unique devices (Scan {self.scan_count})"
176+
)
177+
else:
178+
print(f"Ignored BLE {event=}")
179+
except Exception as e:
180+
sys.print_exception(e)
181+
print(f"Error in BLE IRQ handler {event=}: {e}")

0 commit comments

Comments
 (0)