The Status panel now distinguishes the app-to-server reachability from the server-to-radio link, and uses the v1 nodeResponsive flag to show three radio states: Online (green), Online (idle, orange) and Offline (red).
MeshMonitor Chat for iOS
iOS chat client for MeshMonitor (Meshtastic), Swift port of meshmonitor-chat.el.
Connects to the MeshMonitor REST API v1 with a Bearer token, browses channels, nodes and direct messages, and lets you read and send messages from your phone.
LoRa Radio MeshMonitor iOS
+-----------+ +----------------+ +------------------+
| Meshtastic|----->| Web server |----->| MeshMonitor Chat |
| Node |<-----| REST API |<-----| (this app) |
+-----------+ +----------------+ +------------------+
Physical Proxy + DB SwiftUI
device (port 8080) Polling / Send
Requirements
- macOS with Xcode 16+ (tested with Xcode 26.4 / iOS 26 SDK)
- iOS 17.0 or later target
- XcodeGen to generate the project:
brew install xcodegen
Build
The .xcodeproj is generated from project.yml and is git-ignored. Generate it once:
xcodegen generate
open MeshMonitorChat.xcodeproj
Then build and run from Xcode (⌘R) on a simulator or device.
To build from the command line for the simulator:
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer \
xcodebuild -project MeshMonitorChat.xcodeproj \
-scheme MeshMonitorChat \
-destination 'platform=iOS Simulator,name=iPhone 17' \
build
To install on a real device, you need to sign the app with your Apple ID. Open the project in Xcode, go to Signing & Capabilities, pick your Team and (if needed) change the Bundle Identifier to something unique.
Screens
Setup
Shown on first launch (and from the gear icon afterwards). Asks for host, port, optional HTTPS toggle and the Bearer token. The "Test connection" button calls /api/v1/channels, so it validates both connectivity and the token (a wrong token returns HTTP 401 → Invalid token (unauthorized)). When opened from the gear icon, the form also shows a Status section with server version, connection state, local node, uptime and statistics, plus links to the source code and bug tracker.
Home (tab bar)
A bottom TabView with four tabs: Channels, Nodes, Messages (DMs) and Unread. Each tab is a NavigationStack with the Settings gear in the top-left.
Channels
Lists every usable channel (Primary or Secondary) returned by /api/v1/channels. Tap a channel to enter its chat.
Nodes
All mesh nodes sorted by hop count, with:
- 🟢 / ⚪ online indicator (default threshold: 15 minutes since
lastHeard). - 🔑 icon if the node has exchanged encryption keys (PKC).
- Relative "last heard" time (now / Nm / Nh / Nd).
Tap a node to open a DM. Nodes without PKC show an alert ("DM not possible").
Direct Messages
Active DM conversations, derived from messages with channel = -1. The partner is the other side of the message (the local node is filtered out). Sorted by most recent activity.
Unread
Lists nodes with unread DMs. Counted by comparing each message timestamp against a per-node "last read" timestamp persisted in UserDefaults. Opening a DM marks it as read up to the latest message.
Chat
Same view for channels and DMs:
- Initial fetch: 50 messages.
- Polling every 10 seconds with
since=<unix>for incremental updates. - Send with
POST /api/v1/messages(channelfor channel chats,toNodeIdfor DMs). - Bubbles: own messages right-aligned (accent color, white text), others left-aligned (
systemGray5), good contrast in light and dark mode. - Delivery icon on own messages (
circle.dotted/checkmark/xmark) reflectingdeliveryState/ackFailed. - Auto-scroll to the bottom when the keyboard opens or new messages arrive.
- Return key sends the message; focus stays on the input.
- HTTP 413 → "Message too long" alert. HTTP 503 → "Meshtastic node not connected".
Storage
- Host, port and TLS flag:
UserDefaults. - Bearer token: iOS Keychain (
kSecAttrAccessibleAfterFirstUnlock). - Per-node last-read timestamps for unread counting:
UserDefaults.
Project layout
MeshMonitorChat/
├── App/ # MeshMonitorChatApp + RootView
├── Models/ # Channel, Message, Node, ServerStatus, ChatTarget
├── Services/ # APIClient, Settings, KeychainStore,
│ # MeshDataStore, ReadTracker
├── Views/ # SetupView, WelcomeView, ChannelListView,
│ # NodeListView, DMListView, UnreadListView,
│ # ChatView, MessageRow
├── Resources/ # Assets.xcassets (AppIcon, AccentColor)
└── Info.plist
project.yml # XcodeGen spec
The .xcodeproj is generated from project.yml, never edited by hand.
API endpoints used
| Method | Path | Used by |
|---|---|---|
| GET | /api/v1/status + /api/status (fetched in parallel and merged) |
Status section, Test conn |
| GET | /api/v1/channels |
Channel list, Test connection |
| GET | /api/v1/nodes |
Node list, sender resolution |
| GET | /api/v1/messages?channel=N&limit=&since= |
Channel chat |
| GET | /api/v1/messages?toNodeId=&fromNodeId= |
DM chat (both directions merged) |
| GET | /api/v1/messages?limit=200 |
DM list, Unread aggregation |
| POST | /api/v1/messages (text, channel) |
Send to channel |
| POST | /api/v1/messages (text, toNodeId) |
Send DM |
All requests carry Authorization: Bearer <token>. HTTP 401/403 are mapped to APIError.unauthorized so the UI can show a clear message.
Notes
- Timestamps in messages may arrive as Unix seconds, Unix milliseconds or ISO-8601 strings; the
Messagedecoder normalises all three. NSAllowsLocalNetworkingis enabled so the app can reach local IPs (192.168.x.x) over plain HTTP. Arbitrary public-internet HTTP is blocked; use HTTPS for remote servers.- Implemented: channel and DM read/send, reactions (long-press a message), replies, delivery state, unread counters. Traceroute and position request (which use a session+CSRF API in the upstream) are not implemented yet.
License
GPL-3.0-or-later, matching the upstream Emacs package.