Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
18 changes: 17 additions & 1 deletion meshtastic_visualizer/meshtastic_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -66,16 +68,20 @@ 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

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

Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -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:
Expand Down
89 changes: 84 additions & 5 deletions meshtastic_visualizer/meshtastic_visualizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -126,13 +127,15 @@ 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(
self.update_received_mqtt_log)

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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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()
Expand Down
1 change: 1 addition & 0 deletions meshtastic_visualizer/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class ConnectionKind(enum.Enum):
UNKNOWN=0
SERIAL=1
TCP=2
BLE=3

def create_getter(field_name):
def getter(self):
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ pyserial==3.5
PyQt6-WebEngine==6.7
pyqtgraph
paho-mqtt
cryptography
cryptography
Loading