Initial commit

PyMeshCore GUI is an open-source desktop application for interacting with the MeshCore network.
It focuses on **chatting, prototyping, and development** on top of MeshCore, with an emphasis on
desktop workflows and developer accessibility.

The project is built using PySide6 (Qt for Python) and meshcore-py.
This commit is contained in:
Stefan de Konink
2026-01-04 20:54:12 +01:00
commit 18e21ffcdb
39 changed files with 2342 additions and 0 deletions

222
src/meshcore_gui/main.py Normal file
View File

@@ -0,0 +1,222 @@
import argparse
import asyncio
import signal
import sys
from meshcore import MeshCore
from PySide6.QtCore import QCoreApplication, QObject, QSettings, QThread, QTimer, Slot
from PySide6.QtWidgets import QApplication
from .controllers.channel_controller import ChannelController
from .controllers.contact_controller import ContactController
from .controllers.message_controller import MessageController
from .controllers.meshcore_controller import MeshCoreController
from .utils.async_executor import AsyncExecutor
from .views.main_window import MeshCoreWidget
from .wizard.wizard_companion import CompanionWizard
class ApplicationController(QObject):
def __init__(self, executor, loop):
self.mc = None
self.meshcore_controller = None
self.executor = executor
self.loop = loop
def attempt_connection(self):
async def init_mc(port, interace_type: str):
try:
if interface_type == "serial":
mc = await MeshCore.create_serial(port)
return mc
elif interface_type == "bluetooth":
mc = await MeshCore.create_ble(port)
return mc
except:
pass
return None
async def disconnect_mc(mc):
try:
await mc.disconnect()
except:
pass
if self.mc is not None:
future = asyncio.run_coroutine_threadsafe(disconnect_mc(self.mc), self.loop)
self.mc = None
port = None
settings = QSettings()
settings.beginGroup("interface")
interface_type = settings.value("type", False)
if interface_type == "serial":
port = settings.value("port")
elif interface_type == "bluetooth":
port = settings.value("addr")
settings.endGroup()
if port is None:
return None
future = asyncio.run_coroutine_threadsafe(
init_mc(port, interface_type), self.loop
)
self.mc = future.result()
self.meshcore_controller = MeshCoreController(self.mc, self.executor)
return self.mc is not None
def start(self, force_wizard=False):
if force_wizard:
self.start_wizard()
else:
if self.mc is None:
self.attempt_connection()
if self.mc is not None:
self.start_main()
else:
self.start_wizard()
@Slot(bool)
def on_wizard_done(self, success: bool):
if success:
self.start()
def start_wizard(self):
self.wizard = CompanionWizard(self)
self.wizard.completed.connect(self.on_wizard_done)
self.wizard.show()
def start_main(self):
# --- GUI ---
self.widget = MeshCoreWidget()
self.widget.setWindowTitle("MeshCore Chat")
self.widget.resize(400, 600)
self.widget.show()
# --- Controllers ---
self.message_controller = MessageController(self.widget.list_msgs)
self.contact_controller = ContactController(self.widget.list_contacts)
self.channel_controller = ChannelController(self.widget.list_channels)
# --- Signal/Slot Connections ---
self.meshcore_controller.event_received.connect(
self.meshcore_controller.process_event
)
self.meshcore_controller.event_received.connect(
self.message_controller.process_event
)
self.meshcore_controller.event_received.connect(
self.contact_controller.process_event
)
self.meshcore_controller.event_received.connect(
self.channel_controller.process_event
)
self.meshcore_controller.self_info_ready.connect(self.widget.update_self_info)
self.meshcore_controller.message_sent.connect(self.widget.on_sent)
self.contact_controller.selected.connect(self.message_controller.set_chat)
self.contact_controller.selected.connect(self.channel_controller.deselect)
self.contact_controller.selected.connect(self.widget.set_target)
self.channel_controller.selected.connect(self.contact_controller.deselect)
self.channel_controller.selected.connect(self.message_controller.set_chat)
self.channel_controller.selected.connect(self.widget.set_target)
self.message_controller.new_channel_message.connect(
self.channel_controller.new_channel_message
)
self.message_controller.new_private_message.connect(
self.contact_controller.new_private_message
)
self.widget.text_submitted.connect(self.meshcore_controller.send_message)
self.widget.text_submitted.connect(self.message_controller.send_message)
self.widget.button_adv.clicked.connect(self.meshcore_controller.send_adv)
# --- Initial actions ---
self.meshcore_controller.start_message_stream()
self.meshcore_controller.channel_info()
self.meshcore_controller.get_self_info()
self.meshcore_controller.fetch_contacts()
def main():
parser = argparse.ArgumentParser(description="MeshCore GUI")
parser.add_argument(
"--port", default="/dev/ttyACM0", help="Serial port for the MeshCore device"
)
parser.add_argument(
"--wizard", default=False, action="store_true", help="Start the wizard"
)
args = parser.parse_args()
if args.port is not None:
settings = QSettings()
settings.beginGroup("interface")
settings.setValue("port", args.port)
settings.endGroup()
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(True)
# --- asyncio loop in background thread ---
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop_thread = QThread()
def start_loop():
asyncio.set_event_loop(loop)
loop.run_forever()
loop_thread.run = start_loop
loop_thread.start()
executor = AsyncExecutor(loop)
controller = ApplicationController(executor, loop)
controller.start(args.wizard)
# --- Idempotent shutdown ---
shutdown_called = False
def shutdown(*args):
nonlocal shutdown_called
if shutdown_called:
return
shutdown_called = True
if controller.meshcore_controller:
controller.meshcore_controller.shutdown()
loop.call_soon_threadsafe(loop.stop)
loop_thread.quit()
loop_thread.wait()
app.quit()
# --- Signal Handlers ---
signal.signal(signal.SIGINT, shutdown)
app.aboutToQuit.connect(shutdown)
# Timer to allow Python's signal handler to run
timer = QTimer()
timer.start(500)
timer.timeout.connect(lambda: None)
sys.exit(app.exec())
if __name__ == "__main__":
QCoreApplication.setOrganizationName("Bliksem Labs B.V.")
QCoreApplication.setOrganizationDomain("bliksemlabs.com")
QCoreApplication.setApplicationName("MeshCoreGUI")
main()