From 98b7becfdcc7dd32bd86127939ac93c46e500788 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Wed, 18 Feb 2026 21:21:15 +0100 Subject: [PATCH] WIP: New app: "Scan Bluetooth" 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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 W)a3I|P(m~S0000