From 88b310a4223d187173f2ce84107ce093ccecb5d1 Mon Sep 17 00:00:00 2001 From: antlas0 <150958268+antlas0@users.noreply.github.com> Date: Thu, 22 May 2025 19:54:25 +0200 Subject: [PATCH] various changes * main change is adding a bluetooth interface to collect data from nodes * enhanced the connection groupbox behavior a bit --- README.md | 1 + meshtastic_visualizer/meshtastic_manager.py | 18 ++- .../meshtastic_visualizer.py | 89 +++++++++++- meshtastic_visualizer/resources.py | 1 + requirements.txt | 2 +- resources/app.ui | 128 +++++++++++++++++- 6 files changed, 227 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index cfd5391..ff739ce 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ $ docker build . -t meshtastic_visualizer:latest $ docker run -it \ --env="DISPLAY=$DISPLAY" \ --privileged \ + --volume="/var/run/dbus/:/var/run/dbus/" \ --volume="/tmp/.X11-unix:/tmp/.X11-unix:rw" \ --device=/dev/ttyACM0 \ meshtastic_visualizer:latest \ diff --git a/meshtastic_visualizer/meshtastic_manager.py b/meshtastic_visualizer/meshtastic_manager.py index 241d5a3..79fbc57 100644 --- a/meshtastic_visualizer/meshtastic_manager.py +++ b/meshtastic_visualizer/meshtastic_manager.py @@ -13,6 +13,7 @@ import meshtastic import meshtastic.serial_interface import meshtastic.tcp_interface +from meshtastic.ble_interface import BLEInterface from meshtastic import channel_pb2, portnums_pb2, mesh_pb2, config_pb2, telemetry_pb2 from PyQt6.QtCore import pyqtSignal, QObject @@ -50,6 +51,7 @@ class MeshtasticManager(QObject, threading.Thread): notify_local_device_configuration_signal = pyqtSignal(str) notify_new_packet = pyqtSignal(Packet) notify_message_signal = pyqtSignal() + notify_ble_devices_signal = pyqtSignal(list) notify_traceroute_signal = pyqtSignal(list, list, list) notify_channels_signal = pyqtSignal() notify_nodes_update = pyqtSignal(MeshtasticNode) @@ -66,9 +68,10 @@ def __init__(self): self._interface: Optional[meshtastic.serial_interface.SerialInterface] = None self._is_serial_connected = False self._is_tcp_connected = False + self._is_ble_connected = False def is_connected(self) -> bool: - return self._is_serial_connected or self._is_tcp_connected + return self._is_serial_connected or self._is_tcp_connected or self._is_ble_connected def is_serial_connected(self) -> bool: return self._is_serial_connected @@ -76,6 +79,9 @@ def is_serial_connected(self) -> bool: def is_tcp_connected(self) -> bool: return self._is_tcp_connected + def is_ble_connected(self) -> bool: + return self._is_ble_connected + def set_store(self, store: MeshtasticDataStore) -> None: self._data = store @@ -85,6 +91,11 @@ def get_data_store(self) -> MeshtasticDataStore: def get_meshtastic_devices(self) -> List[str]: return list_serial_ports() + @run_in_thread + def ble_scan_devices(self) -> None: + devices = BLEInterface.scan() + self.notify_ble_devices_signal.emit(devices) + @run_in_thread def connect_device(self, connection_kind:ConnectionKind, target:str, load_db: bool = True) -> bool: res = False @@ -98,6 +109,8 @@ def connect_device(self, connection_kind:ConnectionKind, target:str, load_db: bo if target.startswith("http://"): target = target.replace("http://", "") self._interface = meshtastic.tcp_interface.TCPInterface(hostname=target) + if connection_kind == ConnectionKind.BLE: + self._interface = meshtastic.ble_interface.BLEInterface(target) except Exception as e: trace = f"Failed to connect to Meshtastic device {target}: {str(e)}" self.notify_frontend_signal.emit(MessageLevel.ERROR, trace) @@ -110,6 +123,8 @@ def connect_device(self, connection_kind:ConnectionKind, target:str, load_db: bo self._is_serial_connected = True if connection_kind == ConnectionKind.TCP: self._is_tcp_connected = True + if connection_kind == ConnectionKind.BLE: + self._is_ble_connected = True self.retrieve_channels() node = self._interface.getMyNodeInfo() @@ -148,6 +163,7 @@ def disconnect_device(self) -> bool: trace = f"Meshtastic device disconnected." self._is_serial_connected = False self._is_tcp_connected = False + self._is_ble_connected = False self.notify_frontend_signal.emit(MessageLevel.INFO, trace) res = True finally: diff --git a/meshtastic_visualizer/meshtastic_visualizer.py b/meshtastic_visualizer/meshtastic_visualizer.py index 32606dc..568d91b 100644 --- a/meshtastic_visualizer/meshtastic_visualizer.py +++ b/meshtastic_visualizer/meshtastic_visualizer.py @@ -39,6 +39,7 @@ class MeshtasticQtApp(QtWidgets.QMainWindow): connect_device_signal = pyqtSignal(ConnectionKind, str, bool) disconnect_device_signal = pyqtSignal() + scan_ble_devices_signal = pyqtSignal() get_nodes_signal = pyqtSignal() send_message_signal = pyqtSignal(MeshtasticMessage) retrieve_channels_signal = pyqtSignal() @@ -89,7 +90,7 @@ def __init__(self): self._current_output_folder = self._settings.value("output_folder", os.getcwd()) self._store = MeshtasticDataStore() self._manager = MeshtasticManager() - self._update_meshtastic_devices() + self._update_meshtastic_serial_devices() self.setup_ui() self._manager.set_store(self._store) @@ -126,6 +127,7 @@ def __init__(self): self.update_channels_table) self._manager.notify_nodes_update.connect( self.update_nodes) + self._manager.notify_ble_devices_signal.connect(self._update_meshtastic_ble_devices) self._mqtt_manager.notify_nodes_update.connect( self.update_nodes) self._mqtt_manager.notify_mqtt_logs.connect( @@ -133,6 +135,7 @@ def __init__(self): self.connect_device_signal.connect(self._manager.connect_device) self.disconnect_device_signal.connect(self._manager.disconnect_device) + self.scan_ble_devices_signal.connect(self._manager.ble_scan_devices) self.send_message_signal.connect(self._manager.send_text_message) self.retrieve_channels_signal.connect(self._manager.retrieve_channels) self.get_nodes_signal.connect(self.update_nodes_map) @@ -173,26 +176,56 @@ def set_status(self, loglevel: MessageLevel, message: str) -> None: if loglevel.value == MessageLevel.INFO.value or loglevel.value == MessageLevel.UNKNOWN.value: self.notification_bar.setText(message) - def _update_meshtastic_devices(self) -> None: + def _update_meshtastic_serial_devices(self) -> None: self.device_combobox.clear() for i, device in enumerate(self._manager.get_meshtastic_devices()): self.device_combobox.insertItem(i, device) + def _request_meshtastic_ble_devices(self) -> None: + self.set_status(MessageLevel.INFO, "Scanning bluetooth devices.") + self.ble_scan_button.setText("⌛ BLE Scan") + self.ble_address_combobox.clear() + self.ble_connect_button.setEnabled(False) + self.ble_scan_button.setEnabled(False) + self.scan_ble_devices_signal.emit() + + def _update_meshtastic_ble_devices(self, devices:list) -> None: + if len(devices) == 1: + self.ble_address_combobox.insertItem(0, devices[0].address) + self.ble_address_combobox.setCurrentText(devices[0].address) + else: + for i, device in enumerate(devices): + self.ble_address_combobox.insertItem(i, device.address) + self.set_status(MessageLevel.INFO, f"Found {len(devices)} bluetooth device(s).") + self.ble_scan_button.setText("🔍 BLE Scan") + self.ble_connect_button.setEnabled(True) + self.ble_scan_button.setEnabled(True) + def setup_ui(self) -> None: self.mynodeinfo_refresh_button.clicked.connect(self._manager.get_local_node_infos) self.device_combobox.setCurrentText(self._settings.value("serial_port", "")) + if self._settings.value("ble_address", ""): + self.ble_address_combobox.insertItem(0, self._settings.value("ble_address", "")) + self.ble_address_combobox.setCurrentText(self._settings.value("ble_address", "")) self.tabWidget.currentChanged.connect(self.remove_notification_badge) self.notification_bar.setOpenExternalLinks(True) self.serial_connect_button.clicked.connect(self.connect_device_serial) self.tcp_connect_button.clicked.connect(self.connect_device_tcp) + self.ble_connect_button.clicked.connect(self.connect_device_ble) self.output_folder_button.clicked.connect(self.choose_output_folder) self.output_folder_label.setReadOnly(True) self.output_folder_label.setText(os.path.basename(self._current_output_folder)) - self.scan_com_button.clicked.connect(self._update_meshtastic_devices) + self.serial_scan_button.clicked.connect(self._update_meshtastic_serial_devices) + self.ble_scan_button.clicked.connect(self._request_meshtastic_ble_devices) self.serial_disconnect_button.clicked.connect(self.disconnect_device) self.tcp_disconnect_button.clicked.connect(self.disconnect_device) + self.ble_disconnect_button.clicked.connect(self.disconnect_device) self.load_nodedb_checkbox.stateChanged.connect(self.load_nodedb_checkbox_bis.setChecked) + self.load_nodedb_checkbox.stateChanged.connect(self.load_nodedb_checkbox_ter.setChecked) self.load_nodedb_checkbox_bis.stateChanged.connect(self.load_nodedb_checkbox.setChecked) + self.load_nodedb_checkbox_bis.stateChanged.connect(self.load_nodedb_checkbox_ter.setChecked) + self.load_nodedb_checkbox_ter.stateChanged.connect(self.load_nodedb_checkbox.setChecked) + self.load_nodedb_checkbox_ter.stateChanged.connect(self.load_nodedb_checkbox_bis.setChecked) self.refresh_map_button.clicked.connect(self.get_nodes) self.send_button.clicked.connect(self.send_message) self.nm_update_button.setEnabled(False) @@ -221,7 +254,9 @@ def setup_ui(self) -> None: self.serial_connect_button.setEnabled(True) self.serial_disconnect_button.setEnabled(False) self.tcp_connect_button.setEnabled(True) + self.ble_connect_button.setEnabled(True) self.tcp_disconnect_button.setEnabled(False) + self.ble_disconnect_button.setEnabled(False) self._action_buttons = [ self.send_button, self.message_textedit, @@ -367,20 +402,27 @@ def refresh_ui(self) -> None: self._lock.acquire() self.serial_connect_button.setEnabled(True) self.serial_disconnect_button.setEnabled(False) + self.serial_scan_button.setEnabled(True) self.tcp_connect_button.setEnabled(True) self.tcp_disconnect_button.setEnabled(False) + self.ble_connect_button.setEnabled(True) + self.ble_disconnect_button.setEnabled(False) + self.ble_scan_button.setEnabled(True) for button in self._action_buttons: button.setEnabled(False) self.connection_tabs.setTabEnabled(0, True); self.connection_tabs.setTabEnabled(1, True); + self.connection_tabs.setTabEnabled(2, True); if self._manager.is_serial_connected(): + self.serial_scan_button.setEnabled(False) self.serial_connect_button.setEnabled(False) self.serial_disconnect_button.setEnabled(True) for button in self._action_buttons: button.setEnabled(True) self.connection_tabs.setTabEnabled(0, True); self.connection_tabs.setTabEnabled(1, False); + self.connection_tabs.setTabEnabled(2, False); if self._manager.is_tcp_connected(): self.tcp_connect_button.setEnabled(False) @@ -389,6 +431,17 @@ def refresh_ui(self) -> None: button.setEnabled(True) self.connection_tabs.setTabEnabled(0, False); self.connection_tabs.setTabEnabled(1, True); + self.connection_tabs.setTabEnabled(2, False); + + if self._manager.is_ble_connected(): + self.ble_scan_button.setEnabled(False) + self.ble_connect_button.setEnabled(False) + self.ble_disconnect_button.setEnabled(True) + for button in self._action_buttons: + button.setEnabled(True) + self.connection_tabs.setTabEnabled(0, False); + self.connection_tabs.setTabEnabled(1, False); + self.connection_tabs.setTabEnabled(2, True); if self._mqtt_manager.is_connected(): self.mqtt_connect_button.setEnabled(False) @@ -423,16 +476,27 @@ def refresh_status_header( self._lock.release() def connect_device_serial(self): + self.connection_tabs.setTabEnabled(0, True); + self.connection_tabs.setTabEnabled(1, False); + self.connection_tabs.setTabEnabled(2, False); + self.serial_scan_button.setEnabled(False) + self.serial_connect_button.setEnabled(False) + self.serial_disconnect_button.setEnabled(False) device_path = self.device_combobox.currentText() if device_path: self.set_status(MessageLevel.INFO, f"Connecting to {device_path}.") self.connect_device_signal.emit(ConnectionKind.SERIAL, device_path, self.load_nodedb_checkbox.isChecked()) self._settings.setValue("serial_port", self.device_combobox.currentText()) else: - self.set_status(MessageLevel.ERROR, - f"Cannot connect. Please specify a device path.") + self.set_status(MessageLevel.ERROR, f"Cannot connect. Please specify a device path.") + self.serial_connect_button.setEnabled(True) def connect_device_tcp(self): + self.connection_tabs.setTabEnabled(0, False); + self.connection_tabs.setTabEnabled(1, True); + self.connection_tabs.setTabEnabled(2, False); + self.tcp_connect_button.setEnabled(False) + self.tcp_disconnect_button.setEnabled(False) ip = self.ipaddress_textedit.text() if ip: if "https" in ip: @@ -444,6 +508,21 @@ def connect_device_tcp(self): else: self.set_status(MessageLevel.ERROR, f"Cannot connect. Please specify an accessible ip address.") + def connect_device_ble(self): + self.connection_tabs.setTabEnabled(0, False); + self.connection_tabs.setTabEnabled(1, False); + self.connection_tabs.setTabEnabled(2, True); + self.ble_scan_button.setEnabled(False) + self.ble_connect_button.setEnabled(False) + self.ble_disconnect_button.setEnabled(False) + ble_address = self.ble_address_combobox.currentText() + if ble_address: + self.set_status(MessageLevel.INFO, f"Connecting to {ble_address}.") + self._settings.setValue("ble_address", self.ble_address_combobox.currentText()) + else: + self.set_status(MessageLevel.INFO,f"Connecting to first detected device.") + self.connect_device_signal.emit(ConnectionKind.BLE, ble_address, self.load_nodedb_checkbox_ter.isChecked()) + def disconnect_device(self) -> None: for i, device in enumerate(self._manager.get_meshtastic_devices()): self.device_combobox.clear() diff --git a/meshtastic_visualizer/resources.py b/meshtastic_visualizer/resources.py index eac4a25..57d76eb 100644 --- a/meshtastic_visualizer/resources.py +++ b/meshtastic_visualizer/resources.py @@ -49,6 +49,7 @@ class ConnectionKind(enum.Enum): UNKNOWN=0 SERIAL=1 TCP=2 + BLE=3 def create_getter(field_name): def getter(self): diff --git a/requirements.txt b/requirements.txt index 2170e44..ef5e1a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,4 @@ pyserial==3.5 PyQt6-WebEngine==6.7 pyqtgraph paho-mqtt -cryptography \ No newline at end of file +cryptography diff --git a/resources/app.ui b/resources/app.ui index 6bf7c5f..9047482 100644 --- a/resources/app.ui +++ b/resources/app.ui @@ -1014,7 +1014,7 @@ - Local device connection + Serial connection @@ -1101,7 +1101,7 @@ - Reset nodeDB of node when connecting. + Load nodeDB of node when connecting. Load NodeDB @@ -1110,7 +1110,7 @@ true - + 320 @@ -1135,7 +1135,7 @@ - Remote device connection + TCP connection @@ -1216,7 +1216,7 @@ - Reset nodeDB of node when connecting. + Load nodeDB of node when connecting. Load NodeDB @@ -1226,6 +1226,124 @@ + + + Bluetooth Connection + + + + + 200 + 40 + 111 + 25 + + + + + Liberation sans + 10 + + + + Disconnect. + + + âšī¸ Disconnect + + + + + + 200 + 10 + 111 + 25 + + + + + Liberation sans + 10 + + + + Connect with BLE connection. + + + â–ļī¸ Connect + + + + + + 10 + 40 + 121 + 23 + + + + + Liberation sans + 10 + + + + Load nodeDB of node when connecting. + + + Load NodeDB + + + true + + + + + + 10 + 10 + 181 + 25 + + + + + Liberation sans + 10 + + + + Choose a BLE device. + + + BLE address + + + + + + 320 + 10 + 121 + 25 + + + + + Liberation sans + 10 + + + + List all detected bluetooth devices. + + + 🔍 BLE Scan + + +