From 4826063f36b36c5f1e309b5417afc979420ccef5 Mon Sep 17 00:00:00 2001 From: Jens Diemer Date: Sat, 21 Feb 2026 07:38:00 +0100 Subject: [PATCH] WIP: New app: "Scan Bluetooth" (#58) Just display a table and list all nearby Bluetooth devices --- .../META-INF/MANIFEST.JSON | 24 +++ .../assets/scan_bluetooth.py | 142 ++++++++++++++++++ .../res/mipmap-mdpi/icon_64x64.png | Bin 0 -> 5992 bytes 3 files changed, 166 insertions(+) create mode 100644 internal_filesystem/apps/com.micropythonos.scan_bluetooth/META-INF/MANIFEST.JSON create mode 100644 internal_filesystem/apps/com.micropythonos.scan_bluetooth/assets/scan_bluetooth.py create mode 100644 internal_filesystem/apps/com.micropythonos.scan_bluetooth/res/mipmap-mdpi/icon_64x64.png diff --git a/internal_filesystem/apps/com.micropythonos.scan_bluetooth/META-INF/MANIFEST.JSON b/internal_filesystem/apps/com.micropythonos.scan_bluetooth/META-INF/MANIFEST.JSON new file mode 100644 index 00000000..bc61ab72 --- /dev/null +++ b/internal_filesystem/apps/com.micropythonos.scan_bluetooth/META-INF/MANIFEST.JSON @@ -0,0 +1,24 @@ +{ +"name": "ScanBluetooth", +"publisher": "MicroPythonOS", +"short_description": "Scan Bluetooth", +"long_description": "Lists all nearby Bluetooth devices with some information", +"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.scan_bluetooth/icons/com.micropythonos.scan_bluetooth_0.0.1_64x64.png", +"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.scan_bluetooth/mpks/com.micropythonos.scan_bluetooth_0.0.1.mpk", +"fullname": "com.micropythonos.scan_bluetooth", +"version": "0.0.1", +"category": "development", +"activities": [ + { + "entrypoint": "assets/scan_bluetooth.py", + "classname": "ScanBluetooth", + "intent_filters": [ + { + "action": "main", + "category": "launcher" + } + ] + } + ] +} + diff --git a/internal_filesystem/apps/com.micropythonos.scan_bluetooth/assets/scan_bluetooth.py b/internal_filesystem/apps/com.micropythonos.scan_bluetooth/assets/scan_bluetooth.py new file mode 100644 index 00000000..6b894b46 --- /dev/null +++ b/internal_filesystem/apps/com.micropythonos.scan_bluetooth/assets/scan_bluetooth.py @@ -0,0 +1,142 @@ +""" +Initial author: https://github.com/jedie +https://docs.micropython.org/en/latest/library/bluetooth.html +""" + +import time + +import bluetooth +import lvgl as lv +from micropython import const +from mpos import Activity + +SCAN_DURATION = const(1000) # Duration of each BLE scan in milliseconds +_IRQ_SCAN_RESULT = const(5) + + +# BLE Advertising Data Types (Standardized by Bluetooth SIG) +_ADV_TYPE_NAME = const(0x09) + + +def decode_field(payload: bytes, adv_type: int) -> list: + results = [] + i = 0 + payload_len = len(payload) + while i < payload_len: + length = payload[i] + if length == 0 or i + length >= payload_len: + break + field_type = payload[i + 1] + if field_type == adv_type: + results.append(payload[i + 2 : i + length + 1]) + i += length + 1 + return results + + +class BluetoothScanner: + def __init__(self, device_callback): + self.device_callback = device_callback + self.ble = bluetooth.BLE() + self.ble.irq(self.ble_irq_handler) + + def __enter__(self): + print("Activating BLE") + self.ble.active(True) + return self + + def ble_irq_handler(self, event: int, data: tuple) -> None: + if event == _IRQ_SCAN_RESULT: + addr_type, addr, adv_type, rssi, adv_data = data + addr = ":".join(f"{b:02x}" for b in addr) + names = decode_field(adv_data, _ADV_TYPE_NAME) + name = str(names[0], "utf-8") if names else "Unknown" + self.device_callback(addr, rssi, name) + + def scan(self, duration_ms: int): + print(f"BLE scanning for {duration_ms}ms...") + self.ble.gap_scan(duration_ms, 20000, 10000) + + def __exit__(self, exc_type, exc_val, exc_tb): + print("Deactivating BLE") + self.ble.active(False) + + +def set_dynamic_column_widths(table, font=None, padding=8): + font = font or lv.font_montserrat_14 + for col in range(table.get_column_count()): + max_width = 0 + for row in range(table.get_row_count()): + value = table.get_cell_value(row, col) + width = lv.text_get_width(value, len(value), font, lv.TEXT_FLAG.NONE) + if width > max_width: + max_width = width + table.set_column_width(col, max_width + padding) + + +def set_cell_value(table, *, row: int, values: tuple): + for col, value in enumerate(values): + table.set_cell_value(row, col, value) + + +class ScanBluetooth(Activity): + refresh_timer = None + + def onCreate(self): + screen = lv.obj() + screen.set_flex_flow(lv.FLEX_FLOW.COLUMN) + screen.set_style_pad_all(0, 0) + screen.set_size(lv.pct(100), lv.pct(100)) + + self.table = lv.table(screen) + set_cell_value( + self.table, + row=0, + values=("pos", "MAC", "RSSI", "count", "Name"), + ) + set_dynamic_column_widths(self.table) + + self.mac2column = {} + self.mac2counts = {} + + self.scanner_cm = BluetoothScanner(device_callback=self.scan_callback) + self.scanner = self.scanner_cm.__enter__() # Activate BLE + + self.setContentView(screen) + + def scan_callback(self, addr, rssi, name): + if not (column_index := self.mac2column.get(addr)): + column_index = len(self.mac2column) + 1 + self.mac2column[addr] = column_index + self.mac2counts[addr] = 1 + else: + self.mac2counts[addr] += 1 + + set_cell_value( + self.table, + row=column_index, + values=( + str(column_index), + addr, + f"{rssi} dBm", + str(self.mac2counts[addr]), + name, + ), + ) + + def onResume(self, screen): + super().onResume(screen) + + def update(timer): + self.scanner.scan(SCAN_DURATION) + set_dynamic_column_widths(self.table) + time.sleep_ms(SCAN_DURATION + 100) # Wait ? + print(f"Scan complete: {len(self.mac2column)} unique devices") + + self.refresh_timer = lv.timer_create(update, SCAN_DURATION + 1000, None) + + def onPause(self, screen): + super().onPause(screen) + self.scanner.__exit__(None, None, None) # Deactivate BLE + if self.refresh_timer: + self.refresh_timer.delete() + self.refresh_timer = None diff --git a/internal_filesystem/apps/com.micropythonos.scan_bluetooth/res/mipmap-mdpi/icon_64x64.png b/internal_filesystem/apps/com.micropythonos.scan_bluetooth/res/mipmap-mdpi/icon_64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..f9f8f4884390eacb8fa7362bc57dad726e194486 GIT binary patch literal 5992 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 literal 0 HcmV?d00001