MeshCore One Architecture
June 19, 2026 ยท View on GitHub
MeshCore One is built using a modern, three-tier modular architecture designed for high performance, reliability, and maintainability. It leverages Swift 6's strict concurrency model, actor isolation, and modern iOS frameworks.
Platform Requirements
- Minimum Deployment: iOS 18.0
- Feature Target: iOS 26.0 (uses
#availablechecks for newer APIs) - Swift: 6.2+
- Xcode: 26.0+
High-Level Architecture
The project is divided into three main layers:
- MeshCore (Protocol Layer): A pure Swift implementation of the MeshCore mesh networking protocol.
- MC1Services (Business Logic Layer): Manages higher-level business logic, actor isolation, and shared persistence.
- MeshCore One (UI Layer): The SwiftUI-based user interface and application state management.
graph TD
subgraph MeshCore One [UI Layer]
AppState[AppState @Observable @MainActor]
Views[SwiftUI Views]
MEB[MessageEventDispatcher @MainActor]
LSVM[LineOfSightViewModel]
ES[ElevationService actor]
LS[LocationService @MainActor @Observable class]
LPS[LinkPreviewService final class]
LES[LogExportService enum]
end
subgraph MC1Services [Business Logic Layer]
ServiceContainer[ServiceContainer DI]
CM[ConnectionManager @MainActor @Observable]
SC[SyncCoordinator actor]
subgraph CoreServices [Core Services]
MS[MessageService actor]
CS[ContactService actor]
CH[ChannelService actor]
SS[SettingsService actor]
AS[AdvertisementService actor]
MPS[MessagePollingService actor]
BPS[BinaryProtocolService actor]
DS[DeviceService actor]
HRS[HeardRepeatsService actor]
RXS[RxLogService actor]
end
subgraph RemoteServices [Remote Node Services]
RNS[RemoteNodeService actor]
RAS[RepeaterAdminService actor]
RSS[RoomServerService actor]
end
subgraph IndependentServices [Independent Services]
KS[KeychainService actor]
NS[NotificationService @MainActor @Observable class]
DLB[DebugLogBuffer actor]
RS[ReactionService actor]
end
PS[PersistenceStore @ModelActor actor]
end
subgraph MeshCore [Protocol Layer]
MCS[MeshCoreSession actor]
ED[EventDispatcher]
PB[PacketBuilder enum]
PP[PacketParser enum]
end
subgraph Transport [Transport Layer - in MC1Services]
BT[iOSBLETransport]
BSM[BLEStateMachine]
end
subgraph WiFiTransportLayer [Transport Layer - in MeshCore]
WFT[WiFiTransport]
end
Views --> AppState
AppState --> CM
AppState --> MEB
LSVM --> ES
AppState --> LS
CM --> ServiceContainer
CM --> SC
CM --> MCS
ServiceContainer --> MS
ServiceContainer --> CS
ServiceContainer --> CH
ServiceContainer --> SS
ServiceContainer --> AS
ServiceContainer --> MPS
ServiceContainer --> BPS
ServiceContainer --> RNS
ServiceContainer --> RAS
ServiceContainer --> RSS
ServiceContainer --> KS
ServiceContainer --> NS
ServiceContainer --> RXS
ServiceContainer --> DLB
ServiceContainer --> RS
ServiceContainer --> DS
ServiceContainer --> HRS
SC --> MS
SC --> CS
SC --> CH
SC --> MPS
MEB --> SC
MS --> MCS
CS --> MCS
CH --> MCS
SS --> MCS
AS --> MCS
MPS --> MCS
BPS --> MCS
RNS --> MCS
RAS --> RNS
RSS --> RNS
RNS --> KS
MCS --> ED
CM --> BT
BT --> MCS
MCS --> PB
MCS --> PP
BT --> BSM
CM --> WFT
WFT --> MCS
MS --> PS
CS --> PS
CH --> PS
MPS --> PS
AS --> PS
BPS --> PS
RNS --> PS
RAS --> PS
RSS --> PS
DS --> PS
HRS --> PS
1. MeshCore (Protocol Layer)
The foundation of the project, responsible for low-level communication with MeshCore devices.
- Actor-Based:
MeshCoreSessionis an actor that serializes all device communication, ensuring thread safety. - Event-Driven: Uses an
EventDispatcherto broadcastMeshEvents viaAsyncStream. - Stateless Protocol Handlers:
PacketBuilderandPacketParserare stateless enums that handle the binary encoding/decoding of the Companion Radio Protocol. - Transport Abstraction: The
MeshTransportprotocol allows for different underlying transports.MockTransportandWiFiTransportlive in MeshCore. MC1Services providesiOSBLETransport(CoreBluetooth +BLEStateMachine) for production BLE on iOS, plusSimulatorMockTransportfor the simulator. - Dual Transport Support: Supports both Bluetooth Low Energy (BLE) and WiFi transports simultaneously. Each transport has its own state machine for managing connection lifecycle.
- Auto-Reconnection: Both transports automatically reconnect when connection is lost, with configurable retry logic and backoff strategies.
- LPP Telemetry: Includes a full implementation of the Cayenne Low Power Payload (LPP) for efficient sensor data transmission.
See: MeshCore API Reference | BLE Transport Guide
2. MC1Services (Business Logic Layer)
Bridges the protocol layer and the UI, handling complex business rules and data persistence.
- Service-Oriented: Business logic is divided into specialized actors (
MessageService,ContactService,ChannelService,RemoteNodeService, etc.). - Dependency Injection:
ServiceContainermanages the creation and wiring of all services, providing a single point of initialization for the service layer. - Sync Coordination:
SyncCoordinatororchestrates the connection lifecycle and data synchronization through contacts, channels, and messages phases. - Actor Isolation: Every service is an actor, protecting internal state and coordinating asynchronous operations safely.
- Persistence: Uses SwiftData for local storage. Data is isolated per radio using the
radioID: UUIDfield (an opaque partition key minted once on first pair, distinct from the volatile BLE peripheral UUID) as a namespace, ensuring each radio has its own isolated data. - DTO Pattern: Sendable Data Transfer Objects (
MessageDTO,ContactDTO,DeviceDTO, etc.) enable safe cross-actor data transfer while maintaining strict concurrency compliance. - Connection Management:
ConnectionManager(a@MainActorobservable class) manages the lifecycle of the connection, including AccessorySetupKit pairing, BLE/WiFi transport selection, auto-reconnection, and service wiring. - Message Polling:
MessagePollingServicepulls messages from the device queue and routes them to appropriate handlers.
ServiceContainer & Dependency Injection
The ServiceContainer is the central dependency injection container for MC1Services. It creates and manages all services, handling the dependency graph and lifecycle:
Independent Services (no service dependencies):
KeychainService: Secure credential storage using system keychainNotificationService: Local notification management for message alertsReactionService: Reaction parsing, indexing, and pending-queue handling
Note: ElevationService, LocationService, LinkPreviewService, and LogExportService are app-scoped utilities in the MeshCore One UI layer, not part of the MC1Services package.
Core Services (depend on session/dataStore):
ContactService: Contact management and synchronizationMessageService: Message sending, receiving, and retry logicChannelService: Channel (group) configuration and messagingSettingsService: Device settings and configuration managementAdvertisementService: Advertisement broadcasting and path discoveryMessagePollingService: Automatic message fetching from device queueBinaryProtocolService: Binary protocol operations (telemetry, status, etc.)DeviceService: Device information and managementHeardRepeatsService: Tracking message repeat counts for channel propagation analysisRxLogService: RF packet capture and logging for network diagnosticsDebugLogBuffer: Buffered debug logging persistence forPersistentLogger
Remote Node Services (depend on other services):
RemoteNodeService: Remote node session management and authenticationRepeaterAdminService: Repeater administration and configurationRoomServerService: Room server operations and message routing
Three-Phase Lifecycle:
ServiceContainer is fully wired by its init: all services and their cross-service callbacks are created and connected during construction (constructor injection, with shared coordinators like SyncCoordinator and ContactCleanupCoordinator passed into the services that need them). A separate event-monitoring phase activates the live device-event listeners once a radio is connected.
// Construction: create and wire all services (constructor injection)
let container = ServiceContainer(session: meshCoreSession, modelContainer: modelContainer)
// Start event monitoring when device connects
await container.startEventMonitoring(radioID: radioID)
Construction (init):
All services are created with their direct dependencies (session, dataStore, shared coordinators, other services). The dependency order is:
PersistenceStore(from modelContainer)- Independent services:
KeychainService,NotificationService,SyncCoordinator - Core services:
HeardRepeatsService,RxLogService,RemoteNodeService,ContactService(withSyncCoordinatorand aContactCleanupCoordinator),MessageService,ChannelService,SettingsService,DeviceService,AdvertisementService,MessagePollingService,BinaryProtocolService,DebugLogBuffer,ReactionService,NodeConfigService,NodeSnapshotService - Remote Node services:
RepeaterAdminService,RoomServerService(needRemoteNodeService)
Cross-service connections that can't be plain constructor scalars (path management during message retry, UI-refresh notifications via SyncCoordinator, the contact cleanup coordinator, pushing channel secrets to RxLogService, forwarding heard-repeat events) are all established here during construction. init also sets DebugLogBuffer.shared = debugLogBuffer so PersistentLogger can enqueue entries from anywhere.
Per-connection callbacks that depend on app-layer state (notification action forwarders, the channel-update handler, ConnectionUI sync-activity callbacks) are installed separately by AppState.wireServicesIfConnected() on each connection, not by ServiceContainer.init.
Event Monitoring (startEventMonitoring(radioID:)):
Activates event listeners on services that process live device events. Guarded by a tri-state lifecycle (stopped/starting/active/stopping) so overlapping callers cannot double-start. Only called after a device is connected:
HeardRepeatsService.configure(radioID:localNodeName:): needs device info for local node identificationAdvertisementService.startEventMonitoring(radioID:): listens for advertisement/path discovery events (gated onenableAdvertisementMonitoring)RxLogService.startEventMonitoring(radioID:): captures RF packets for diagnostic viewerMessageService.startEventMonitoring()andstartAckExpiryChecking(): listen for ACK events and expire stale sends during send-with-retryRemoteNodeService.startEventMonitoring(): listens for remote node session eventsMessagePollingService.startMessageEventMonitoring(radioID:): routes polled messages to handlersMessagePollingService.startAutoFetch(radioID:): begins periodic message polling (gated onenableAutoFetch)- Debug log pruning (keeps most recent 1,000 entries)
- Node snapshot pruning (removes snapshots older than 1 year)
Monitoring is stopped symmetrically by stopEventMonitoring() on disconnect, which also flushes the debug log buffer.
DTO (Data Transfer Object) Pattern
To comply with Swift 6's strict concurrency model, all SwiftData models have corresponding Sendable DTO types for cross-actor communication:
Model-to-DTO Conversion:
MessageDTO: Sendable snapshot ofMessagefor passing between actorsContactDTO: Sendable snapshot ofContactfor UI and service communicationDeviceDTO: Sendable snapshot ofDevicefor device informationChannelDTO: Sendable snapshot ofChannelfor channel configurationRemoteNodeSessionDTO: Sendable snapshot ofRemoteNodeSessionfor session stateRoomMessageDTO: Sendable snapshot ofRoomMessagefor room communications
Usage Pattern:
// In actor-isolated service
func fetchMessages(radioID: UUID) async -> [MessageDTO] {
let messages = await dataStore.fetchMessages(radioID: radioID)
return messages.map { MessageDTO(from: \$0) }
}
// In @MainActor UI code
let messageDTOs = await messageService.fetchMessages(radioID: radioID)
// DTOs are Sendable, safe to use across actor boundaries
Benefits:
- Concurrency Safety: DTOs are
Sendable, enabling safe cross-actor transfers - Immutability: Snapshot semantics prevent unintended mutations
- Actor Isolation: Services work with SwiftData models; UI and callbacks receive DTOs
- Clean Boundaries: Clear separation between persistence and business logic layers
See: MC1Services API Reference | Sync Guide | Messaging Guide
Localization Boundary
The MC1Services package contains no localization resources by design: strings defined there (service errorDescription text, displayName enum properties, log messages) are developer-facing English. Anything shown to the user must be mapped to the SwiftGen L10n enum at the view layer, either through a small app-target extension on the service type (e.g. NotificationLevel.localizedName) or, for strings the service layer must emit itself such as notification content, through the NotificationStringProvider bridge.
3. MeshCore One (UI Layer)
A modern SwiftUI application that provides a user-friendly experience for mesh messaging.
- AppState: A central
@Observableclass that manages app-wide state, navigation, and coordination between the UI and services. - MessageEventDispatcher: A
@MainActorobject that subscribes toSyncCoordinator.dataEvents()and feeds aMessageEventStream, bridging service-layer events to SwiftUI's@MainActorcontext for real-time UI updates. - SwiftUI & Modern APIs: Built with the latest SwiftUI features, utilizing
@Observable, environment injection, and modern navigation. - Onboarding Flow: A guided experience for permissions, discovery, and device pairing.
- iMessage-Style Chat: Rich messaging interface with delivery status, timestamps, and metadata (SNR, path length).
- MapKit Integration: Displays contact locations on a map with real-time updates and type-based markers.
See: [MeshCore One API Reference](api/MeshCore One.md) | User Guide
Concurrency & Data Flow
Concurrency Model
MeshCore One strictly adheres to the Swift 6 concurrency model:
- Actors: Used for all services and session management to prevent data races.
- MainActor: UI updates,
ConnectionManager, andMessageEventDispatcherare isolated to the main thread. - AsyncStream: Used for all event-driven communication (BLE data -> Protocol events -> Business events -> UI updates).
- Structured Concurrency: Utilizes
TaskGroupsfor complex asynchronous flows like message retries and contact synchronization.
nonisolated(unsafe) Invariants
The service-layer packages use nonisolated(unsafe) only where Swift's isolation checking can't be satisfied another way, with safety maintained by convention:
BLEStateMachine, CBCentralManager (BLEStateMachine.swift):
var centralManager: CBCentralManager!- Pattern: CBCentralManager is not
Sendable, but needs nonisolated access for thebluetoothStatecomputed property - Safety: Mutated once during initialization (assigned in init). All subsequent access is either from the actor's isolated context or from the
bluetoothStateproperty, which reads the atomicstateproperty of CBCentralManager. Returns.unknownduring the brief window before assignment
BackupUserDefaults, static keypath mapping tables (BackupUserDefaults.swift):
static let boolMappings/stringMappings- Pattern:
WritableKeyPathis notSendable, so the constant mapping tables are markednonisolated(unsafe) - Safety: Immutable
letconstants, never mutated after initialization
(SyncCoordinator's callbacks are now @MainActor-isolated with setter methods, and DebugLogBuffer.shared is backed by an OSAllocatedUnfairLock, so neither uses nonisolated(unsafe) any longer. The MeshCore One app layer carries one additional use for a map font-name constant.)
Data Flow (Receiving a Message)
- BLE Transport:
BLEStateMachinereceives a notification from CoreBluetooth and yields the data toiOSBLETransport'sreceivedDatastream. - Session Parser:
MeshCoreSessionreceives the data, usesPacketParserto create aMeshEvent, and dispatches it viaEventDispatcher. - Service Processing:
MessagePollingServicereceives the event, routes it to the appropriate handler based on message type. - Persistence: The handler creates a
MessageDTOand saves it toPersistenceStore. - UI Update:
SyncCoordinatoryields aSyncDataEvent;MessageEventDispatcherreceives it, feeds theMessageEventStream, updating observable state and triggering SwiftUI view updates.
Data Flow (Sending a Message)
- User Action: User types message in
ChatConversationViewand taps send. - Service Call:
MessageService.sendMessageWithRetry()is called. - Queue: Message is saved to
PersistenceStorewith status.pending. - Retry Loop:
MessageService.sendMessageWithRetry()attempts delivery usingMessageServiceConfig.default(up to 5 attempts: 4 direct, switching to flood routing after 4 failed direct attempts, then 1 flood attempt). - ACK Tracking: Each send attempt waits inline for ACK using
waitForEvent()with configurable timeout. - Completion: ACK received ->
.delivered, or all attempts exhausted ->.failed.
Persistence Layer
- Technology: SwiftData
- Isolation: Each MeshCore radio has its own isolated data store using
radioID: UUID(an opaque partition key minted once on first pair) as the namespace, ensuring privacy and preventing data mixing. - Models:
Device: Metadata, radio parameters, and capabilities. Theidfield is the live-connection handle (BLE peripheral identifier, or a derived UUID for WiFi-bridged radios) and is volatile;publicKeyandradioIDsurvive re-pair and backup round-trips.Contact: Public keys, names, types (Chat, Repeater, Room), and location data. ContainsradioIDfor isolation.Message: Content, timestamps, delivery status, and mesh metadata (SNR, Path). ContainsradioIDfor isolation.Channel: Slot-based configuration for group messaging. ContainsradioIDfor isolation.RemoteNodeSession: Tracks authenticated sessions with remote repeater/room nodes.RoomMessage: Messages exchanged in room server conversations.
- DTOs: Each model has a corresponding
SendableDTO for safe cross-actor transfers.
Debug Logging Infrastructure
MeshCore One includes a comprehensive debug logging system designed to help diagnose issues in the field without requiring a developer machine.
Architecture
The debug logging system is implemented as a two-layer pipeline:
-
Logging Entry Point (
PersistentLogger):- Lightweight struct that writes to OSLog and enqueues debug entries
- Used throughout services for consistent, structured logging
-
Buffered Persistence (
DebugLogBuffer):- Actor that batches log entries and writes to SwiftData
- Flushes every 5 seconds or when 50 entries are queued
- Logs are pruned on connect to keep the most recent 1,000 entries
Usage Pattern
// Log from any context (OSLog + buffered persistence)
logger.info("Connected to device")
logger.warning("High packet loss detected")
logger.error("Failed to parse packet: \(error.localizedDescription)")
Log Categories
Logs are categorized for easy filtering:
- Connection: BLE/WiFi connection lifecycle, pairing, reconnection
- Messaging: Message sending, delivery, retries, ACK tracking
- Sync: Contact/channel synchronization, data updates
- Transport: Protocol events, packet parsing, transport errors
- UI: Navigation, user actions, state changes
- Diagnostics: Line of Sight, Trace Path, RX Log operations
Export & Analysis
Debug logs can be exported for analysis:
- LogExportService: Builds a text export with device/app metadata
- Time Range: Last 24 hours of persisted logs (up to 1,000 entries)
- Sharing: Exported file can be shared via the system share sheet
iPad Split-View Architecture
On iPad, MeshCore One uses a split-view layout that provides a more efficient workflow and better use of available screen real estate.
Navigation Pattern
The split-view implementation follows Apple's iPad interface guidelines:
- Two-Panel Layout: List panel (left) and detail panel (right)
- Independent Navigation Stacks: Each panel maintains its own navigation state
- Tab Coordination: Chats, Nodes, Tools, and Settings use split-view; Map uses a single
NavigationStack - Responsive Design: Automatically adjusts layout based on orientation and window size
Implementation Details
Navigation state is managed by NavigationCoordinator, one of AppState's extracted sub-objects:
@Observable class AppState {
let connectionUI = ConnectionUIState() // Status pills, sync activity, alerts
let batteryMonitor = BatteryMonitor() // Battery polling, thresholds
let onboarding = OnboardingState() // Onboarding flag and path
let navigation = NavigationCoordinator() // Tab selection, pending navigation
}
Views access sub-objects directly (e.g., appState.navigation.selectedTab). Cross-tab navigation uses a pending navigation mechanism โ the source view sets a pending target on NavigationCoordinator, which the destination view picks up on tab switch.
Panel Independence
- List Panel: Shows master list (chats, contacts, map annotations)
- Detail Panel: Shows selected item (conversation, contact details, map location)
- State Separation: Changes in right panel don't affect left panel state
- Concurrent Updates: Both panels can update independently as data changes
Adaptive Behavior
- Portrait: Stacked panels (list on top, detail below)
- Landscape: Side-by-side panels (list on left, detail on right)
- Regular Size Class: Always shows both panels
- Compact Size Class: Shows single panel with full-screen navigation
Diagnostic Tools Architecture
MeshCore One includes several diagnostic tools built on a shared infrastructure for RF analysis and network troubleshooting.
Shared Services
- Elevation Service: Fetches terrain elevation data from Open-Meteo API
- RF Calculator: Core calculations for signal propagation, Fresnel zones, and link budget
- Location Service: Manages location permissions and current position for analysis
Line of Sight Tool
Components:
LineOfSightView: Main SwiftUI view for analysis interfaceLineOfSightViewModel: State management and business logicRFCalculator: RF propagation calculations (path loss, Fresnel zone clearance)SegmentAnalysis: Terrain segment analysis for obstruction detectionFresnelZoneRenderer: Canvas-based visualization of Fresnel zonesTerrainProfileCanvas: Custom canvas for terrain profile visualization
Workflow:
- User selects target contact or enters coordinates
- Elevation data is fetched along path (Open-Meteo API)
- Terrain profile is generated from elevation samples
- Fresnel zones are calculated for given frequency
- Clearance analysis determines signal quality (green/yellow/red)
- Visual results are rendered with clearance status
Trace Path Tool
Components:
TracePathView: Main view for path discoveryTracePathViewModel: Path discovery and management logicSavedPathsViewModel: Management of saved routing pathsPathEditingSheet: Interactive path editor with repeater selection
Workflow:
- User initiates trace path from contact detail
- App discovers available repeaters and routes to target
- Path results are displayed with signal quality per hop
- User can edit path by selecting different repeaters
- Saved paths are persisted for future use and can be visualized on map
RX Log Viewer
Components:
RxLogView: Live packet capture viewerRxLogViewModel: Packet capture and filtering logicRxLogService: Service for packet capture and persistence
Workflow:
- User opens RX Log viewer
- Service starts capturing RF packets from transport layer
- Packets are displayed in real-time with metadata
- User can filter by packet type, source, or destination
- Logs can be exported for offline analysis
Further Reading
API References
- MeshCore API
- MC1Services API
- [MeshCore One API](api/MeshCore One.md)