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
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,25 @@ messages to any reachable node, and local message history that survives
restarts — Meshtastic radios don't store message history themselves, so
OrcMesh keeps its own.

### ⚙ USB radio controls

When connected over USB/Serial, the Device page provides typed controls for
radio and module configuration, owner identity, channels, fixed position,
reboot/shutdown, NodeDB reset, and guarded factory resets. Passwords, PINs,
PSKs, and cryptographic key material are never read into editable controls or
written to logs; secret replacements are write-only.

The Firmware tab discovers official `meshtastic/firmware` releases for the
target reported by the connected radio. It verifies the release SHA-256,
target/hardware metadata, and image hashes before flashing. Update mode writes
only the application image and preserves settings. Full install requires a
typed confirmation, erases the device, and restores the verified factory,
OTA, and filesystem images at the release-declared offsets. A chip-ID preflight
must identify the expected ESP32 family before either path can write anything.

Firmware flashing is inherently interruption-sensitive. Use a known-good USB
cable and do not unplug or power off the radio until OrcMesh reports completion.

### 📶 Spectrum *(optional — needs an RTL-SDR)*

A waterfall view of raw RF energy in the LoRa ISM bands. This shows *that*
Expand Down Expand Up @@ -152,8 +171,6 @@ before reaching the UI or the SQLite store. The map is Leaflet in a
## Roadmap

- MeshCore network support alongside Meshtastic
- Editing LoRa region/preset from the app (with appropriate guard rails —
region selection has real RF-regulatory meaning)
- Compact image transmission over the mesh — see
[docs/mcoreimg-integration.md](docs/mcoreimg-integration.md)

Expand Down
9 changes: 5 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ below — they're cited here for provenance, not as a dependency.
but never made it into a downloadable build until this one; also the
first release with a proper Windows installer (`MeshChat-Setup-*.exe`,
Inno Setup) instead of a raw `dist/` folder
- USB device controls for typed radio/module configuration, owner identity,
channels, fixed position, maintenance actions, and guarded resets
- Official Meshtastic firmware discovery and verified ESP32-S3 update/full
flashing, with target matching, chip-ID preflight, secret-safe logging, and
explicit destructive confirmation

## In progress

Expand All @@ -64,7 +69,6 @@ Not yet built, in no particular priority order:
- Multiple profiles
- Multiple simultaneously connected local radios
- Message search
- Channel configuration (from the app)
- QR/channel URL import (needs explicit security warnings per the spec —
channel URLs embed the PSK)
- Windows installer is unsigned (`packaging/installer.iss`, Inno Setup) —
Expand All @@ -86,14 +90,11 @@ Not yet built, in no particular priority order:
first step: an offline IQ-capture-then-decode PoC using `lora-phy` to
validate feasibility before any UI is built around it — the Spectrum page
currently shows *that* RF activity exists, not decoded packet content.
- **Editing LoRa region/preset from the app.** Needs real guard rails —
region selection has RF-regulatory meaning, not just a UI preference.
- **Compact image transmission over the mesh** — see
[docs/mcoreimg-integration.md](docs/mcoreimg-integration.md).

## Explicitly out of scope (per the original build spec, §1/§26)

- Firmware flashing
- Reimplementing Meshtastic protobuf framing (use the official `meshtastic`
package unless an upstream defect makes that unavoidable)
- Storing/logging channel PSKs, Bluetooth PINs, or Wi-Fi credentials
1 change: 1 addition & 0 deletions THIRD_PARTY_LICENSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package, which is GPL-3.0-only copyleft.
| [numpy](https://numpy.org/) | BSD-3-Clause | Numerics |
| [platformdirs](https://github.com/platformdirs/platformdirs) | MIT | Per-user data/log paths |
| [PyPubSub](https://github.com/schollii/pypubsub) | BSD-2-Clause | Event distribution |
| [esptool](https://github.com/espressif/esptool) | GPL-2.0-or-later | ESP32 firmware flashing |
| [pyrtlsdr](https://github.com/pyrtlsdr/pyrtlsdr) | GPL-3.0 | Optional — RTL-SDR spectrum capture |

## Bundled web assets
Expand Down
8 changes: 7 additions & 1 deletion packaging/orcmesh.spec
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ datas += [(str(ROOT / "src/meshchat/ui/map/web"), "meshchat/ui/map/web")]
# 2. Meshtastic protobuf definitions (*.proto files shipped with the package)
datas += collect_data_files("meshtastic", includes=["*.proto", "*.pyi"])

# 3. PySide6 WebEngine resources (icudtl.dat, qtwebengine_resources.pak, etc.)
# 3. esptool flasher stubs (JSON payloads loaded at runtime)
datas += collect_data_files("esptool")

# 4. PySide6 WebEngine resources (icudtl.dat, qtwebengine_resources.pak, etc.)
# PyInstaller's PySide6 hook usually handles these, but be explicit for safety.
try:
import PySide6
Expand Down Expand Up @@ -80,6 +83,9 @@ hiddenimports += ["pubsub.core", "pubsub.utils"]
hiddenimports += collect_submodules("pyqtgraph")
hiddenimports += ["pyqtgraph.graphicsItems.PlotItem.PlotItem"]

# Official Meshtastic ESP32 firmware flashing
hiddenimports += collect_submodules("esptool")

Comment thread
coderabbitai[bot] marked this conversation as resolved.
# platformdirs
hiddenimports += ["platformdirs", "platformdirs.windows"]

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies = [
"platformdirs>=4,<5",
"pyqtgraph>=0.13",
"numpy>=1.26",
"esptool>=5,<6",
]

[project.scripts]
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ PyPubSub>=4.0
platformdirs>=4,<5
pyqtgraph>=0.13
numpy>=1.26
esptool>=5,<6

# Optional — RTL-SDR spectrum waterfall (Spectrum page).
# Also needs the native librtlsdr driver; on Windows install the WinUSB
Expand Down
96 changes: 96 additions & 0 deletions src/meshchat/controllers/firmware_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Threaded facade for firmware network I/O and flashing."""
from __future__ import annotations

import logging

from PySide6.QtCore import QObject, QThread, Signal, Slot

from meshchat.services.firmware import discover_release, flash_bundle, prepare_bundle


log = logging.getLogger(__name__)


class _FirmwareWorker(QObject):
release_found = Signal(object)
progress = Signal(int, int)
bundle_ready = Signal(object)
log = Signal(str)
completed = Signal(str, bool, str)

@Slot(str, bool)
def discover(self, pio_env: str, include_prerelease: bool) -> None:
try:
release = discover_release(pio_env, include_prerelease)
self.release_found.emit(release)
self.completed.emit("discover", True, f"Found Meshtastic {release.version}")
except Exception as exc:
log.exception("Firmware release discovery failed")
self.completed.emit("discover", False, str(exc))

@Slot(object, str, str)
def prepare(self, release, pio_env: str, hw_model: str) -> None:
try:
bundle = prepare_bundle(
release, pio_env, hw_model,
lambda done, total: self.progress.emit(done, total),
)
self.bundle_ready.emit(bundle)
self.completed.emit("prepare", True, "Firmware downloaded and verified")
except Exception as exc:
log.exception("Firmware preparation failed")
self.completed.emit("prepare", False, str(exc))

@Slot(object, str, bool, object)
def flash(self, bundle, port: str, full_install: bool, expected_usb) -> None:
try:
flash_bundle(
bundle, port, full_install, expected_usb,
lambda line: self.log.emit(line),
)
self.completed.emit("flash", True, "Firmware flash completed")
except Exception as exc:
log.exception("Firmware flash failed")
self.completed.emit("flash", False, str(exc))


class FirmwareController(QObject):
release_found = Signal(object)
progress = Signal(int, int)
bundle_ready = Signal(object)
log = Signal(str)
completed = Signal(str, bool, str)

_discover_requested = Signal(str, bool)
_prepare_requested = Signal(object, str, str)
_flash_requested = Signal(object, str, bool, object)

def __init__(self, parent=None):
super().__init__(parent)
self._worker = _FirmwareWorker()
self._thread = QThread(self)
self._worker.moveToThread(self._thread)
self._worker.release_found.connect(self.release_found)
self._worker.progress.connect(self.progress)
self._worker.bundle_ready.connect(self.bundle_ready)
self._worker.log.connect(self.log)
self._worker.completed.connect(self.completed)
self._discover_requested.connect(self._worker.discover)
self._prepare_requested.connect(self._worker.prepare)
self._flash_requested.connect(self._worker.flash)
self._thread.start()

def discover(self, pio_env: str, include_prerelease: bool = False) -> None:
self._discover_requested.emit(pio_env, include_prerelease)

def prepare(self, release, pio_env: str, hw_model: str) -> None:
self._prepare_requested.emit(release, pio_env, hw_model)

def flash(self, bundle, port: str, full_install: bool, expected_usb) -> None:
self._flash_requested.emit(bundle, port, full_install, expected_usb)

def shutdown(self) -> None:
self._thread.quit()
if not self._thread.wait(5000):
log.warning("Firmware operation still active; waiting for it to finish")
self._thread.wait()
Loading
Loading