Skip to content

feat: Refactor MeshService into smaller, single-responsibility components#4108

Merged
jamesarich merged 3 commits into
mainfrom
refactor/decompose-service-squashed
Jan 2, 2026
Merged

feat: Refactor MeshService into smaller, single-responsibility components#4108
jamesarich merged 3 commits into
mainfrom
refactor/decompose-service-squashed

Conversation

@jamesarich

Copy link
Copy Markdown
Collaborator

This refactors the monolithic MeshService into smaller, more focused, and testable components. The goal is to improve maintainability, clarify responsibilities, and facilitate future development by adopting a more modular architecture.

Key changes include:

  • MeshService: Stripped down to a lifecycle and binding coordinator. Most of its logic has been moved to new, specialized classes.
  • MeshRouter: A new central class that orchestrates various handlers for incoming packets.
  • Component Breakdown:
    • ConnectionStateHandler: Manages and broadcasts the device connection state (renamed from MeshServiceConnectionStateHolder).
    • MeshConnectionManager: Handles connection lifecycle events (connect, disconnect, sleep), notifications, and starts/stops other services like location updates.
    • MeshConfigHandler: Manages device and module configuration state.
    • MeshConfigFlowManager: Orchestrates the multi-stage config/node-info download process.
    • MeshNodeManager: Manages the node database, including caching, updates, and lookups.
    • MeshDataHandler: Processes application-layer data packets (e.g., text messages, positions, telemetry).
    • MeshMessageProcessor: Acts as the entry point for all incoming radio data, handling early packet buffering and dispatching to the appropriate router or handler.
    • MeshCommandSender: Centralizes the logic for creating and sending commands and data packets to the radio.
    • MeshLocationManager: Manages location updates from the device's GPS.
    • MeshMqttManager: Handles MQTT proxy functionality.
    • MeshHistoryManager: Manages store-and-forward history requests.
    • MeshTracerouteHandler & MeshNeighborInfoHandler: New dedicated handlers for their respective packet types.
  • Exception Hierarchy: BLEException and its related classes are now subclasses of RadioNotConnectedException for more consistent error handling.
  • Constants: Centralized broadcast action strings and other constants into Constants.kt.
  • Testing: Added new unit tests for the refactored components (MeshNodeManagerTest, MeshCommandSenderTest, MeshDataMapperTest) and updated existing tests.

…ents

This commit refactors the monolithic `MeshService` into smaller, more focused, and testable components. The goal is to improve maintainability, clarify responsibilities, and facilitate future development by adopting a more modular architecture.

Key changes include:
- **`MeshService`**: Stripped down to a lifecycle and binding coordinator. Most of its logic has been moved to new, specialized classes.
- **`MeshRouter`**: A new central class that orchestrates various handlers for incoming packets.
- **Component Breakdown**:
    - **`ConnectionStateHandler`**: Manages and broadcasts the device connection state (renamed from `MeshServiceConnectionStateHolder`).
    - **`MeshConnectionManager`**: Handles connection lifecycle events (connect, disconnect, sleep), notifications, and starts/stops other services like location updates.
    - **`MeshConfigHandler`**: Manages device and module configuration state.
    - **`MeshConfigFlowManager`**: Orchestrates the multi-stage config/node-info download process.
    - **`MeshNodeManager`**: Manages the node database, including caching, updates, and lookups.
    - **`MeshDataHandler`**: Processes application-layer data packets (e.g., text messages, positions, telemetry).
    - **`MeshMessageProcessor`**: Acts as the entry point for all incoming radio data, handling early packet buffering and dispatching to the appropriate router or handler.
    - **`MeshCommandSender`**: Centralizes the logic for creating and sending commands and data packets to the radio.
    - **`MeshLocationManager`**: Manages location updates from the device's GPS.
    - **`MeshMqttManager`**: Handles MQTT proxy functionality.
    - **`MeshHistoryManager`**: Manages store-and-forward history requests.
    - **`MeshTracerouteHandler` & `MeshNeighborInfoHandler`**: New dedicated handlers for their respective packet types.
- **Exception Hierarchy**: `BLEException` and its related classes are now subclasses of `RadioNotConnectedException` for more consistent error handling.
- **Constants**: Centralized broadcast action strings and other constants into `Constants.kt`.
- **Testing**: Added new unit tests for the refactored components (`MeshNodeManagerTest`, `MeshCommandSenderTest`, `MeshDataMapperTest`) and updated existing tests.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
@github-actions github-actions Bot added the refactor no functional changes label Dec 31, 2025
@codecov

codecov Bot commented Dec 31, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0.00%. Comparing base (8de3806) to head (8d0b670).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@          Coverage Diff          @@
##            main   #4108   +/-   ##
=====================================
  Coverage   0.00%   0.00%           
=====================================
  Files          2       2           
  Lines         19      19           
  Branches       7       7           
=====================================
  Misses        19      19           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@jamesarich

jamesarich commented Dec 31, 2025

Copy link
Copy Markdown
Collaborator Author

MeshService Refactor: Complete Architectural Summary & Comparison

This document provides a comprehensive technical overview and "Before & After" comparison of the MeshService refactor. The goal was to decompose a 3,100+ line monolith into a modular, specialized architecture while ensuring strict lifecycle management and 100% functional parity.


🏗️ Architectural Overview

The core of the refactor was the shift from a "God Object" model to a Delegated Pipeline.

Summary of Shifts

Feature Before (Monolith) After (Modular)
Class Size ~3,100 lines ~350 lines (Orchestrator only)
Packet Routing Inline when blocks in MeshService Specialized MeshRouter & Handlers
Binder Logic Implemented directly in IMeshService.Stub Delegated to MeshActionHandler
Lifecycle Manual Job management per component Unified start(scope) propagation
State Management Scattered mutable variables Centralized ServiceRepository & NodeManager

🛡️ Stability & Lifecycle "Tightening"

One of the primary goals was to ensure zero resource leaks.

  • Unified Scope: Every handler now implements a start(scope: CoroutineScope) method. The MeshService creates a single serviceScope in onCreate and cancels it in onDestroy, ensuring all background work (MQTT flows, packet queues, location updates) halts simultaneously.
  • Concurrency: State management now relies on ConcurrentHashMap and AtomicLong, making the service resilient to high-frequency radio events.
  • Standardized Errors: A centralized Exceptions.kt ensures that all mesh-specific errors correctly inherit from RemoteException for safe Binder propagation.

📉 Lifecycle Comparison

Before: Ad-Hoc Scope Management

In the monolith, serviceScope was often used directly, but its propagation was not standardized. Some handlers created their own jobs or scopes, leading to potential leaks.

graph TD
    MS[MeshService] -- created --> Job[Manual serviceJob: Job]
    MS -- created --> Scope[Manual serviceScope: CoroutineScope]
    
    MS -->|Direct Use| PH[PacketHandler]
    MS -->|Direct Use| HRD[handleReceivedData]
    
    Note["Management was ad-hoc; no enforced start(scope) pattern."]
Loading

After: Standardized Scope Propagation

graph TD
    MS[MeshService] -- creates --> Scope[serviceScope]
    MS -- passes to --> Router
    Router -- spreads to --> DH[DataHandler]
    Router -- spreads to --> CH[ConfigHandler]
    Router -- spreads to --> NH[NeighborInfoHandler]
    MS -- passes to --> NM[NodeManager]
    
    MS -- onStop/Destroy --> Cancel[Scope.cancel]
    Cancel --> AllHandlers[All Handlers Stop Simultaneously]
Loading

Important

This architecture ensures that zero background work continues if the service is stopped, making the app much more battery efficient and stable during rapid reconnects.


🛰️ Data Flow: Incoming Radio Packets

These diagrams illustrate how a packet travels from the radio to its final destination.

Before: The Monolithic Tangle

In the monolithic version, MeshService acted as a "god object," processing all incoming data through nested logic within its own class.

graph TD
    Radio[RadioInterfaceService] -->|ByteArray| MS[MeshService]
    
    subgraph monolith["MeshService Monolith (main branch)"]
        MS -->|handleFromRadio| Proto[MeshProtos.FromRadio]
        Proto -->|PACKET| HRMP[handleReceivedMeshPacket]
        HRMP -->|isReady| PRMP[processReceivedMeshPacket]
        HRMP -->|!isReady| Buffer[EarlyPacketBuffer]
        
        PRMP -->|metadata| HRD[handleReceivedData]
        
        HRD -->|PortNum when| Dispatch{Internal Dispatch}
        
        Dispatch -->|TEXT| Rem[rememberDataPacket]
        Dispatch -->|ADMIN| HRA[handleReceivedAdmin]
        Dispatch -->|POSITION| HRP[handleReceivedPosition]
        Dispatch -->|TELEMETRY| HRT[handleReceivedTelemetry]
        
        HRA -->|inline mutation| DB[(Node DB)]
        HRP -->|inline mutation| DB
        HRT -->|inline mutation| DB
        
        HRD -->|End| BC[broadcastReceivedData]
    end
Loading

After: The Specialized Pipeline

In the refactored version, responsibilities are cleanly separated. Each stage has a specific purpose.

graph TD
    Radio[RadioInterfaceService] -->|ByteArray| MP[MeshMessageProcessor]
    
    subgraph processing["Processing Layer"]
        MP -->|parse| Proto[FromRadio]
        Proto -->|PACKET| HRMP[handleReceivedMeshPacket]
        HRMP -->|isReady| PRMP[processReceivedMeshPacket]
        HRMP -->|!isReady| Buffer[EarlyPacketBuffer]
        
        PRMP -->|metadata| DH[MeshDataHandler]
    end
    
    subgraph handlers["Handlers"]
        DH -->|Data/Text| DH
        DH -->|Admin| CH[MeshConfigHandler]
        DH -->|NeighborInfo| NH[MeshNeighborInfoHandler]
        DH -->|Traceroute| TH[MeshTracerouteHandler]
    end
    
    DH -->|Update| NM[MeshNodeManager]
    CH -->|Update| NM
    NM -->|Persist| Repo[NodeRepository]
    
    DH -->|Notify| SB[MeshServiceBroadcasts]
    SB -->|Intent| OS[Android System]
Loading

⚡ Action Flow: Binder IPC Calls

How client applications interact with the service.

Before: Direct Implementation

The Binder was often used as a dumping ground for logic, leading to a massive service file and difficult-to-trace state changes.

graph LR
    App[Client App] -->|IPC| Stub[IMeshService.Stub]
    Repo[ServiceRepository] -->|ServiceAction| MS[MeshService]
    
    subgraph monolith_ipc["MeshService Monolith (main branch)"]
        Stub -->|Direct Logic| PH[PacketHandler]
        Stub -->|Direct Mutation| DB[(Node DB)]
        
        MS -->|onServiceAction| LocalMethods["Private Methods: favoriteNode, ignoreNode, etc."]
        LocalMethods -->|PacketHandler| Radio[Radio]
    end
Loading

After: Clean Delegation

The MeshService binder is now a clean "relay" layer, delegating all operations to specialized "Action" components.

graph LR
    App[Client App] -->|IPC| Stub[IMeshService.Stub]
    
    subgraph delegation["Delegation Layer"]
        Stub -->|Delegates to| AH[MeshActionHandler]
    end
    
    AH -->|Orchestrates| CS[MeshCommandSender]
    AH -->|Updates| NM[MeshNodeManager]
    AH -->|Triggers| SB[MeshServiceBroadcasts]
Loading

✅ Functional Parity Details

  • Packet Handling: 100% of the when(portnum) logic from the original service was successfully migrated to MeshDataHandler and its sub-handlers (MeshConfigHandler, MeshNeighborInfoHandler, MeshTracerouteHandler).
  • Admin Commands: Remote configuration, owner updates, and device management (Reboot, DFU, Factory Reset) were fully migrated to the MeshActionHandler delegation layer.
  • Service Actions: Favorites, ignored nodes, and reactions are now handled via a clean reactive flow from the ServiceRepository, processed by MeshActionHandler.

Note

This refactor reduces the complexity of the core service loop by orders of magnitude while providing a much more maintainable and testable foundation for future features.


This writeup was clanker generated, but reviewed by me.

Migrated the asynchronous operation handling in `PacketHandler.kt` from `java8.util.concurrent.CompletableFuture` to `kotlinx.coroutines.CompletableDeferred`.

This change modernizes the concurrency model by:
*   Using `withTimeout` and `await()` for cleaner, idiomatic coroutine-based timeout and result handling.
*   Removing the `java.util.concurrent.TimeoutException` in favor of `kotlinx.coroutines.TimeoutCancellationException`.
*   Replacing `isDone` with `isCompleted` for checking the status of the deferred task.
@jamesarich jamesarich force-pushed the refactor/decompose-service-squashed branch from f371433 to 8d0b670 Compare January 2, 2026 15:27
@jamesarich jamesarich marked this pull request as ready for review January 2, 2026 17:12
@jamesarich jamesarich added this pull request to the merge queue Jan 2, 2026
Merged via the queue into main with commit b3ebe76 Jan 2, 2026
6 checks passed
@jamesarich jamesarich deleted the refactor/decompose-service-squashed branch January 2, 2026 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor no functional changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant