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
+
+
+