Skip to content

Repository files navigation

MeshSat

MeshSat for Linux phones: your Meshtastic node over Bluetooth or the LoRa back cover, the Bridge in your pocket, one package.

License: GPL v3 Mobian / Debian 13, arm64 Node: Bluetooth or the LoRa back cover

Install · The node · The Bridge · What is proven · meshsat.net

A Linux phone with the MeshSat Bridge on it is a router between a Meshtastic mesh, a satellite modem and the MeshSat Hub, the way a phone with MeshSat Android or iOS is: it adopts a Meshtastic node over Bluetooth, and a PinePhone with the Pine64 LoRa back cover is a node by itself, which the package notices and uses without being asked. This repository packages all of it for Linux phones: one .deb, installed with one command, and the MeshSat app in the phone's app grid, the same screens as MeshSat Android and iOS.

Status: pre-release. This is a prototype under active development, not a finished product. The node and the Bridge are proven on one phone on one bench, at 0 dBm only: from 5 dBm up the back cover's long frames arrived damaged on the bench, and the phone cannot receive for some seconds after each transmission. The suspected cause is the radio's plain crystal drifting as the amplifier heats; it is not proven, and no hardware remedy has been tried. A radio that stops answering has to be re-seated by hand. The package has never been installed by anyone but its authors and has never been used in an actual emergency. See What is proven, and what is not before you rely on it for anything.

How it fits together

flowchart LR
    mesh["Meshtastic mesh<br/>LoRa 868 MHz"] <--> radio
    subgraph phone["PinePhone, Mobian"]
        radio["SX1262 in the back cover"] <-->|"I2C pogo pins"| daemon["meshtasticd<br/>+ MeshSat bridge layer"]
        daemon <-->|"TCP 4403"| bridge["MeshSat Bridge"]
        bridge <--> app["MeshSat app<br/>GTK4, libadwaita"]
        bridge <-->|"USB-C"| rb["RockBLOCK 9603"]
    end
    rb <-->|"Iridium SBD"| sat(("Iridium"))
    sat <--> hub["MeshSat Hub"]
    bridge <-->|"Wi-Fi, cellular"| hub
Loading

What is here

  • docs/INSTALL.md: the one command, the optional Hub key and satellite modem, what the node does and does not do.
  • app/meshsat/: the MeshSat app, Python with GTK 4 and libadwaita: the screens of MeshSat Android and iOS (Home, Messages, Map, People, Setup and its pages) drawn natively for a phone in the hand, with the same colour tokens, type (IBM Plex), Material icons, words and measures (written in the Android app's dp and scaled once, 1 dp = 0.9 px on a 360 px screen; MESHSAT_APP_SCALE changes it), night mode as the same red-only colour matrix over the whole window, and the map on OpenStreetMap tiles through the Android app's dark-tile matrix (libshumate); satellite passes are the Bridge's predictions for the phone's position (geoclue, the node, or a position typed in) drawn as the Android chart; SMS is the phone's own SIM through the Bridge's ModemManager transport (the lane, the chats, the SOS to the emergency contacts); a mesh node is "Node !id" until the phone has heard its name, as on the other apps, is asked for it again on the cadence its firmware accepts, and a name heard once is kept by the app across restarts of the Bridge. It reads the Bridge's API on 127.0.0.1:6050 and never opens the node's own port. Its tabs and pages open over D-Bus (gapplication action net.meshsat.Bridge tab "'map'", open "'satellite'", night), which is how tools/capture-phone.sh takes the parity captures. The icon in the app grid, the .desktop entry and the AppStream metadata are under package/.
  • package/: the Debian package's control files, maintainer scripts and static files; build-deb.sh assembles the package from the node sources of meshsat-lora-backplate at a pinned commit, a daemon built on a phone from meshsat-firmware, the Bridge out of meshsat's container image (fetch-bridge.sh), and Meshtastic's web client release. No gateway logic lives here.
  • app/meshsat/notify.py (meshsat-notify, a user service of the session): MeshSat outside its window. The notifications MeshSat Android posts, in its words (a text that arrived, a message that did not go, an SOS in progress with Cancel SOS, and the satellite signal as one notification kept up to date, "Iridium signal 3/5"); net.meshsat.Status on the session bus for whatever draws the radios' state outside the app; a tray icon where the shell has a tray (Plasma Mobile, desktops).
  • phosh-plugins/: MeshSat in Phosh, Mobian's shell, whose top bar takes no icons from applications: a tile among the quick settings ("Mesh: 2 nodes", or the satellite's bars and "Satellite: 3/5"; a tap opens the app) and a widget on the lock screen. Built on a phone by phosh-plugins/build.sh (GTK 3's headers and nothing of Phosh: the tile finds Phosh's types by name when Phosh loads it); Phosh finds them at its next start after the install.
  • tools/: capture-phone.sh (the captures above), vector2svg.py (Android vector drawables to the SVG icons the app ships), make-signal-icons.py (the six satellite signal icons), and three bench tools: phone-touch.py (real touch gestures on the phone's touchscreen, for screenshots of what only a finger opens), stand-in-status.py (what the notifier says with a modem at N bars, on a phone without one) and lockscreen-widget-test.py (the lock-screen widget in a window of its own). For the tests: e2e-run.sh (the end-to-end suite on the bench phone, from a machine that reaches it), e2e-farend.py (a Meshtastic radio on a serial port as the far end), record-fixtures.sh (a live Bridge's answers as the scripted Bridge's raw material), android-strings.py and parity-check.py (the words of MeshSat Android and the parity ledger, docs/PARITY.md).
  • tests/: the unit tests (python3 -m unittest discover tests, no GTK needed) and tests/e2e/, the end-to-end tests that run on the phone: headless.sh opens a private session (its own runtime directory, session bus and accessibility bus, a headless phoc at the phone's geometry) so the person's own session is never touched; run.py starts the app under test with its test seams (its own application id and directories, a trace file, no privileged commands), reads its widget tree over AT-SPI and drives it through its actions; fakebridge.py is a scripted Bridge that answers from tests/fixtures/scenarios.py and records every request; the scratch tier starts a second, real Bridge (the packaged binary) on a free port with a database of its own and no device, for what only the real API can show; the live tier runs against the Bridge on the device and a T-Deck at the far end.
  • The package's units: meshsat-hardware.service (at boot, and again when the app asks: is the LoRa back cover on this phone? then /run/meshsat/hardware.env tells the Bridge to use its daemon over TCP, otherwise to adopt a node over Bluetooth, and the daemon and the watchdog stay off), meshtasticd.service (the node, only with the cover), meshsat-radio-watch.timer (the watchdog that tells a user to re-seat a cover whose radio stopped answering, and starts the node again when it answers), meshsat-bridge.service (the Bridge). In Bluetooth mode the app's Setup > Your MeshSat node scans for Meshtastic nodes, pairs with the PIN the node shows, and the Bridge keeps the node across restarts; /etc/meshsat/hardware.conf with MESHSAT_NODE=bluetooth makes a phone with a cover use a node over Bluetooth anyway.

What is proven, and what is not

State
The package installs with one command on a Mobian PinePhone Pro and starts the node, the watchdog and the Bridge Yes, 28 Sep 2026: sudo apt install ./meshsat_0.2.0_arm64.deb, exit 0, the three units active, Final Tx power: 0 dBm, the node's identity carried over; every upgrade since 0.1.0 too
The MeshSat icon in the app grid opens the app, with the Android and iOS apps' screens Yes, 28 Sep 2026: the owner used the first native build on the bench phone and had its scale, icon and map corrected the same day; 0.2.0's captures of every screen are in the release notes
Messages typed in the app reach a T-Deck, and a T-Deck's texts show in the app Yes, 28 Sep 2026: a text a T-Deck on the laptop sent showed in the installed app's chat on the bench phone, and the text the suite typed into the composer was heard by the T-Deck (its log: "from": "!52cb81e7", "text": "e2e 19413", "rx_snr": 3.25; tests/e2e/cases/l_mesh.py, tools/e2e-farend.py). Not yet exercised by a person
An SOS goes out with the person's name and position, its screen shows where it went, a cancellation follows it, and the alarm can be tested Yes, 28 Sep 2026, against the scripted Bridge on the bench phone (the routes, the dialogs, the cancellation, the check-in timer, in Android's words: tests/e2e/cases/h_sos.py); live, the alarm test's text left through the Bridge on the mesh (l_alarm.py). No real SOS has ever been sent from this app, and the SMS and satellite legs are proven against the scripted Bridge only
Message queue, Links and Routing rules work against the Bridge Yes, 28 Sep 2026 (0.7.0), on the bench phone: against a scripted Bridge every state the three screens show; against a second, real Bridge started for the run on a scratch database, a rule made, edited, switched and deleted in the app is the record the Bridge stores, a link switched off in the app is off in the Bridge at once, and a satellite message that cannot leave is cancelled and put back in the queue (tests/e2e/cases/s_*.py); live, a rule made in the app on the phone's own Bridge and removed again (l_rules.py). The switch found a fault in the Bridge, fixed in the Bridge this package carries (MESHSAT-1401). No message has yet been passed on by a rule on the bench: the phone has no second working link (no SIM, no modem, no Hub key)
The rest of Setup > Advanced: mesh topology, audit log, certificates and keys, encrypt or decrypt, diagnostics, node log Yes, 29 Sep 2026 (0.8.0), on the bench phone: every page against a scripted Bridge (tests/e2e/cases/h_advanced.py); against a second, real Bridge, a certificate imported in the app is the one the Bridge stores (its subject, SHA-256 and expiry) and a text a rule passed on is an audit entry the app shows, checks and saves, and the saved copy's hash chain checks on its own (s_advanced.py); the topology and the node log read from the phone's own Bridge and node in the captures. A text encrypted by the Bridge's own construction opens in the app. The log of a node over Bluetooth: next row
Mesh radio settings change the node, and only what was changed Yes, 29 Sep 2026 (0.9.0), on the bench phone: every tab against a scripted Bridge, in cover and in Bluetooth mode (h_radio.py, h_radio_bt.py: each Apply sends only the changed fields, the questions come first, a channel write never carries a key); against a real Bridge with no node, a write is refused with its reason (s_radio.py); live, a scratch Bridge on the phone adopted T-Deck A over Bluetooth, the hop limit set in the app was 4 on the T-Deck when read over its USB cable, its region and keys unchanged, and was put back to 3; the T-Deck's own log streamed to the Node log page while its switch was on (l_radio.py). The phone's own node (the back cover) was only read. This needed a fix in the Bridge this package carries: before it, a settings write answered 200 and changed nothing (MESHSAT-1405)
Messaging and the SMS page write what they say, and nothing else Yes, 29 Sep 2026 (0.9.1), on the bench phone: against a scripted Bridge every switch and button of Messaging becomes the SMS link's chains as the Bridge takes them, a key that cannot encrypt is refused before it goes, compression is offered only where the Bridge can compress (h_messaging.py); against a real Bridge, a link's chains set on their own keep the rest of the link, a bad key is refused, the SMS gateway takes no default number and keeps its secrets (s_messaging.py). No SMS has been encrypted or decrypted live: the phone has no SIM. This needed two Bridge fixes the package carries (MESHSAT-1411: a link set to encrypt sent the text in the clear when its key could not encrypt; MESHSAT-1412)
Zones, offline maps and tracks work as on Android Yes, 29 Sep 2026 (0.10.0), on the bench phone: against a scripted Bridge that holds the zones and checks positions as the Bridge does, a zone placed on the map is saved as Android's 32-point circle, a node crossing it is listed within the 5 s refresh, a zone goes after its question, and without the Bridge's zone service the screen says so (h_zones.py); a detailed map added from a file is used first, a file that is no map is refused and one with vector tiles kept aside in Android's words, and with the tile server cut off the map goes offline after three failed downloads, shows the detailed map and says which, or zooms out to the world overview (h_maps.py); the Map tab draws a track per node from a day of positions, hides a node with its track, and People's "Show on map" goes there (h_tracks.py); against the live Bridge, a zone saved from the app and deleted again (l_zones.py). This needed a Bridge fix the package carries (MESHSAT-1414: the Bridge never created its zone service). No zone has been placed by a real finger yet: the suite presses the map through the app's test action
People's cards, the QR scanner, key bundles and the Hub by QR code or link work as on Android Yes, 29 Sep 2026 (0.11.0), on the bench phone: against a scripted Bridge, My card as the Bridge signs it, with its QR code and fingerprint, a card pasted and added as imported, the three errors in Android's words, a card read through the scanner's camera pipeline and added as scanned, the scanner that keeps looking until a code shows, Forget (h_cards.py); a key bundle pinned on first use and checked against its pin, a changed key asked about first, a 64-hex key taken as the SMS key (h_keys.py); a scanned Hub code claimed through the Hub's "not yet" answers and then asked about, a link asked about first and applied at once, a spent code and an inline code by link refused, a claim dropped on Cancel (h_provision.py); emergency contacts picked from an address book or typed, the same number refused (h_contacts.py); against a real Bridge, My card signed by the Bridge's routing key and read as Android reads it (s_cards.py). The card format holds Android's own test vector byte for byte, in the Bridge's Go test and the app's unit test. This needed a Bridge change the package carries (MESHSAT-1416: the card endpoint). No card has been exchanged with an Android phone yet, and the phone's camera has not read a real code: the suite shows the scanner a picture through the same GStreamer decoder
The Hub page shows the link as it is, and a mailbox check asks first Yes, 29 Sep 2026 (0.11.1), on the bench phone: against a scripted Bridge that reports the link as the Bridge does, each of Android's six states with its dot, "Why: …" only switched on and failed, the Setup row and the Home lane, the switch that writes the setting and restarts nothing, the connection test ("42ms", or "Not connected" with nothing sent), the connection details saved as typed with the Bridge restarted, a claim still waiting on the card (h_hub.py); against a real Bridge, the state with no Hub, the test refused without a link, and the switch and the details kept by the Bridge (s_hub.py); "Check Mailbox" asks first, Cancel sends nothing, and it is not offered without a modem (h_mailbox.py). This needed a Bridge change the package carries (MESHSAT-1417: the link's state and reason, the switch, the test, the fields; a save no longer clears the Hub's CA). No Hub link is live on the bench: the connected and failed states are proven against the scripted Bridge, and the Bridge's own Go tests against a fake broker
Ham radio, TAK and Reticulum are set up as on Android, and nothing goes on the air unasked Yes, 29 Sep 2026 (0.11.2), on the bench phone: against a scripted Bridge that checks the gateways as the Bridge does, the three cards in Android's words, every APRS write with its switch and stored settings (KISS through an external Direwolf), the chips and the beacon written at once, Auto's passcode, APRS-IS with the phone's position, the status rows, the switch kept in the app until there is a callsign, the Bridge's refusal said, TAK's switches and Save, Reticulum's link made, switched and saved (h_integrations.py); against a real Bridge with a TNC, an APRS-IS server and a Reticulum node the case runs on the phone itself, the TNC connected and its station heard with nothing sent to it, Android's APRS-IS login line and the verified reply, TAK without a server, the Reticulum link online (s_integrations.py). This needed a Bridge change the package carries (MESHSAT-1421: APRS-IS, the position beacon, TAK without a server, Reticulum over TCP with TLS). Nothing was sent over the air, to a real APRS-IS server, to ATAK or to the Hub's Reticulum: the bench has no licence, no radio for APRS and no TAK client
Home, the first start, People, About and the Satellite page are as on Android, and a chat can have its own key Yes, 29 Sep 2026 (0.12.0), on the bench phone: against a scripted Bridge, every Home card in Android's order and words (SOS, Message queue with a bar per lane, Your position from the phone's own fix, the two signal charts, the satellite mailbox, recent messages), Getting started with its checks and Hide, Arrange Home kept across a restart (h_home_cards.py); the welcome at the first start only (h_welcome.py); the node banner's states (h_banner.py); People with "(you)", Android's table and sorts (h_people.py); About (h_about.py); the Satellite page for the RockBLOCK on USB-C with its own 9704 card (h_satellite.py); a chat's key section with its six actions and the lock on a sealed text (h_chatkey.py); a new message to a typed number (h_messages.py); against a real Bridge, a chat's key saved, read back and removed (s_chatkey.py) and the Satellite page (s_satellite.py). This needed a Bridge change the package carries (MESHSAT-1423 and MESHSAT-1425: the mailbox check and what it found, the hops a packet took, each modem's signal on its own, a chat's own SMS key, SMS that go out as written). No satellite session was opened and no SMS was sent: the mailbox's outcomes are proven against the scripted Bridge, and the bench has no SIM
An SOS and the alarm test follow every route to the end, and the test's satellite route is a position report Yes, 29 Sep 2026 (1.0.0), on the bench phone: against a scripted Bridge that queues every send as the Bridge does, each route in Android's words from "Waiting to send" to "Sent, and the Hub has it" (h_sos.py); with the phone's position, the test's satellite route is Android's position report and the test text goes on the mesh only (h_sos_position.py); Android's SosRunTest cases ported (tests/test_sos_deliveries.py). This needed a Bridge change the package carries (MESHSAT-1430: the position report, one test at a time, dropped after 30 minutes; MESHSAT-1431: an SOS by satellite past the credit budget). No satellite session was opened: the report is proven in the Bridge's queue, not in the air
An SOS takes every route that is set up, and a leg on a route that is down waits and goes when the route is back Yes, against a scripted Bridge, 30 Sep 2026 (1.0.3), on the bench phone: a satellite modem this phone had before counts as a route while it is not plugged in, its leg shows "Waiting to send" until the modem is back, then "Sent, and the Hub has it"; a blank SOS name takes the Hub callsign in the test, the SOS and the cancellation; the cancellation on the mesh is the Bridge's (h_sos_routes.py, h_sos_screen.py, tests/test_sosflow.py). The Bridge's side (MESHSAT-1447, in the package): each leg a delivery that waits for its link and keeps trying until the SOS is cancelled, a cancelled SOS never going out afterwards, the Hub told over the internet under the satellite frame's alert id, the SOS kept across a restart, in the Bridge's Go tests. No real SOS has ever been sent
Only this phone reaches the Bridge, unless it is shared Yes, 29 Sep 2026 (1.0.0), on the bench phone: from the laptop, the Bridge (6050), the node's API (4403) and Meshtastic's web client (9443) refused after the install; the Bridge reachable once shared and refused again once the switch was off, the node's ports refused throughout; the switch in Diagnostics with its question and the address (h_share.py); the rules loaded in a private network namespace in the unit tests (tests/test_share_package.py)
The screens do what they say against every state of the Bridge Yes, 30 Sep 2026 (1.0.3): 197 scripted cases run the installed app on the phone in a private session against a Bridge that plays states the bench cannot (a modem, a SIM, the Hub, an SOS in progress, a queue in every state, a Bridge that is down or asks for a pause), reading the screen through the accessibility bus; every route opens and fits the screen. Every row of docs/PARITY.md is verified or excluded with its reason, but two the bench cannot prove (a satellite session, SMS without a SIM)
A phone without the cover adopts a Meshtastic node over Bluetooth Yes, 28 Sep 2026, on the bench PinePhone Pro with the cover overridden (MESHSAT_NODE=bluetooth): scan, pairing with the PIN a T-Deck showed, link up in 20 s, the T-Deck's six nodes listed; a text sent through the Bridge went out through the T-Deck, a text from another node over LoRa reached the phone through it; after a restart of the Bridge the node was adopted again by itself. One phone, one node, the app's own PIN dialog not yet exercised by a person
The package finds the LoRa back cover by itself Yes, 28 Sep 2026: with the override removed the phone found its cover at the next install, started the daemon, let the T-Deck's Bluetooth link go, and the Bridge went back to the cover's node
Notifications outside the app Yes, 29 Sep 2026 (1.0.0), on the bench phone against a stand-in notification daemon: a text in Android's words, one per text; a message that did not go, in its lane's words (h_notify_more.py); the satellite signal as one notification kept up to date in place and withdrawn when the modem goes; the alarm test's notification kept in place, withdrawn when the test ends, and a tap on it opens the test (h_alarm_notify.py). Earlier, on 28 Sep 2026: a text from a T-Deck over LoRa gave exactly one notification on the phone
MeshSat in Phosh's quick settings and on its lock screen Yes, module-level, 29 Sep 2026 (1.0.0): both plugins installed where Phosh 0.46 looks for them, and loaded as its loader loads them, in a GTK 3 host of their own on the test session: the tile's words and the lock-screen widget's lines follow the Bridge, a tap on the tile opens Home, and both say when MeshSat is not running (h_phosh.py). The tile was seen among Phosh's quick settings on the bench phone on 28 Sep 2026; the widget has not been seen on the lock screen itself (it cannot be captured while the screen is locked). Other Phosh versions not tried
A tray icon on Plasma Mobile or a desktop Against a stand-in tray host, 29 Sep 2026 (1.0.0): the item registers, its icon and words follow the Bridge, a click opens Home, and it registers again after a tray restart (h_tray.py). On a real Plasma Mobile or desktop tray: not yet tried
The satellite modem of a node adopted over Bluetooth Built, not proven on the air, 29 Sep 2026 (1.0.1): the Satellite page's "Satellite modem on the node" with "Use the node's modem" and the node's health report, against a scripted Bridge (h_satellite_node.py); the Bridge's pipe to the node (MESHSAT-1391) went through four independent reviews and its Go tests model the node's firmware. Never run with a real node: the bench's T-Beam with the RockBLOCK is out of the phone's Bluetooth range
Phones other than the PinePhone Pro, desktops, amd64 Not yet run: an amd64 package (the Bridge and the app, no node daemon: the node is a Meshtastic radio over Bluetooth) builds since 0.5.1 and the pipeline installs it in a bare Debian 13; nobody has used it on a desktop yet, and no phone but one PinePhone Pro has run any package
The node: texts both ways with a T-Deck at 0 dBm Yes, 28 Sep 2026, on the bench of meshsat-lora-backplate
The Bridge talks to the node over TCP Yes, on the phone, 28 Sep 2026: a text sent through the Bridge's API was read on a T-Deck (Received text msg from=0x52cb81e7), and a text typed on the T-Deck reached the Bridge's message store through the daemon
A text from the mesh reaches the Hub through the phone Not yet: needs a Hub API key on the phone
A text from the mesh reaches the satellite through the phone Not yet: needs the RockBLOCK on USB-C
Transmit above 0 dBm Not qualified: from 5 dBm up, the back cover's frames of 126 bytes or more arrived damaged on the bench (0 of 22). The suspected cause is the cover's plain crystal drifting as the amplifier heats; a TCXO is the usual remedy, but it has not been tried and the cause is not proven
Use without a person nearby Not possible as long as only a hand can reset a radio that stopped answering
Published in an app store or an apt repository No, not before a text has gone from the phone to the satellite
Deployment to a real end user, use in an actual emergency Never

Related projects

Licence

GPL-3.0, like the rest of MeshSat. Meshtastic is a registered trademark of Meshtastic LLC; this project is not affiliated with it.

The icons are Google's Material Icons, as MeshSat Android draws them (Apache License 2.0, package/icons/LICENSE-Material-Icons.txt), and the type is IBM Plex (SIL Open Font License 1.1, package/fonts/LICENSE.txt); the package carries both licences under /usr/share/doc/meshsat/.

Releases

Sponsor this project

Packages

Contributors

Languages