chore: clean up brownfield specs and migrate to timestamp naming (#5432)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-05-12 14:22:13 -05:00
committed by GitHub
parent ff9d6881c0
commit 73469b415e
86 changed files with 86 additions and 8256 deletions

View File

@@ -122,7 +122,7 @@ const session = await joinSession({
name: "speckit_load",
description:
"Load the primary artifacts (spec.md, plan.md, tasks.md) for a specific feature spec. " +
"Provide the spec ID (directory name, e.g. '001-local-mesh-discovery') or a partial match. " +
"Provide the spec ID (directory name, e.g. '20260511-211823-compose-screenshot-testing') or a partial match. " +
"Optionally load only specific artifacts.",
parameters: {
type: "object",

View File

@@ -1,61 +1,55 @@
# Skill: Meshtastic Design Standards
## Description
Brand identity, color palette, accessibility, and icon specifications for Meshtastic clients. All UI must comply with these standards.
Android-specific guidance for applying the Meshtastic design standards. All visual rules, color palettes, accessibility requirements, and cross-platform conventions live upstream.
> **Upstream source:** <https://github.com/meshtastic/design> — if this skill diverges from upstream, upstream wins.
> **Source of truth:** [`meshtastic/design/standards/`](https://github.com/meshtastic/design/tree/main/standards)
> Read `meshtastic_design_standards_latest.md` for the full spec (colors, M3 mapping, accessibility, units/locale, agent checklist).
> If this skill diverges from upstream, **upstream wins**.
## 1. Brand Colors
## 1. How to Use the Standards
- **Primary/Foreground:** `#2C2D3C` (RGB 44 45 60)
- **Secondary/Background/Accent:** `#67EA94` (RGB 103 234 148)
Before implementing any UI:
1. **Read the upstream standards** — they include a full agent implementation checklist
2. **Check the settings validation doc** — [`meshtastic/design/validation/settings-validation-android.md`](https://github.com/meshtastic/design/blob/main/validation/settings-validation-android.md) has field-by-field validation rules for every config/module setting
3. Apply the Android-specific mappings below
## 2. Extended Color Palette
## 2. Android / Compose Mappings
### Neutral Scale (derived from Primary `#2C2D3C`)
### Theme Tokens
The upstream standards define M3 role mappings (Section 8). In this codebase:
- Theme is defined in `core/ui/``MeshtasticTheme` composable
- Use M3 tokens (`MaterialTheme.colorScheme.primary`, `.surface`, etc.) — never raw hex values
- Dynamic color (Android 12+): supported in the `google` flavor via `dynamicColorScheme()`
| Name | Hex | Usage |
|------|-----|-------|
| Neutral 950 | `#0F1017` | Darkest background |
| Neutral 900 | `#1A1B26` | Dark mode background |
| Neutral 800 | `#2C2D3C` | **Primary** — dark mode surface / light mode text |
| Neutral 700 | `#3D3E50` | Dark mode elevated surface |
| Neutral 600 | `#555668` | Dark mode secondary text |
| Neutral 500 | `#6E7082` | Placeholder text |
| Neutral 400 | `#9496A6` | Disabled / tertiary |
| Neutral 300 | `#B8BAC8` | Borders (light mode) |
| Neutral 200 | `#D5D6E0` | Dividers |
| Neutral 100 | `#ECEDF3` | Light mode surface / card |
| Neutral 50 | `#F5F6FA` | Light mode background |
### Brand Colors → Compose
| Standard Name | Hex | Compose Usage |
|---------------|-----|---------------|
| Primary | `#2C2D3C` | `MaterialTheme.colorScheme.primary` |
| Accent | `#67EA94` | `MaterialTheme.colorScheme.tertiary` (never as text on light bg) |
| Green 600 | `#3FB86D` | Use for success text on light backgrounds |
| Error | `#E05252` | `MaterialTheme.colorScheme.error` |
| Link | `#9BA8E0` | Blue 400 — for clickable text |
### Green Scale (derived from Accent `#67EA94`)
### Key Rules (Android-specific)
- **Icons:** Use `MeshtasticIcons` (from `core/ui/icon/`), not `material.icons.Icons`
- **Touch targets:** 44×44dp minimum (M3 default is 48dp — compliant)
- **Typography:** Default body = 16sp. Support Dynamic Type via `MaterialTheme.typography`
- **Message bubbles:** Use `onSurface` for text color, never node identity colors
| Name | Hex | Usage |
|------|-----|-------|
| Green 100 | `#E5FCEE` | Success tint background |
| Green 300 | `#B5F5CE` | Light highlight |
| Green 400 | `#8FF0B2` | Hover / active accent |
| Green 500 | `#67EA94` | **Accent** — primary action / brand highlight |
| Green 600 | `#3FB86D` | Text on light backgrounds |
| Green 700 | `#2D8F52` | Strong / dark green text |
## 3. App Icons
### Semantic Colors
- Launcher icons: separate SVGs (foreground/background), 108px square, logo 58px wide/high
- Generate with [Image Asset Studio](https://developer.android.com/studio/write/image-asset-studio#create-adaptive). Name: `ic_launcher2`
- Action bar: `logo/svg/Mesh_Logo_White.svg`, 0% padding, HOLO_DARK theme, named `app_icon`
| Name | Hex | Usage |
|------|-----|-------|
| Info | `#5C6BC0` | Informational indicators / links |
| Info Light | `#E8EAF6` | Info tint background |
| Warning | `#E8A33E` | Caution / attention |
| Warning Light | `#FFF3E0` | Warning tint background |
| Error | `#E05252` | Errors / destructive actions |
| Error Light | `#FDEAEA` | Error tint background |
## 4. Settings Validation Reference
## 3. Accessibility
When implementing or modifying settings screens, consult the upstream validation spec:
[`settings-validation-android.md`](https://github.com/meshtastic/design/blob/main/validation/settings-validation-android.md)
All foreground/background pairings must meet WCAG AA contrast (4.5:1 minimum). Use `Green 600` (`#3FB86D`) or `Green 700` (`#2D8F52`) for green text on light backgrounds — never the raw accent `#67EA94`, which does not meet contrast requirements on white.
## 4. Android App Icons
- Launcher icons use separate SVGs for foreground and background, 108px square, with the logo 58px wide/high.
- Regenerate with [Image Asset Studio](https://developer.android.com/studio/write/image-asset-studio#create-adaptive). Name the icon `ic_launcher2`.
- Action bar icons: import `logo/svg/Mesh_Logo_White.svg` with 0% padding, HOLO_DARK theme, named `app_icon`.
It documents every field's:
- Valid ranges and byte limits
- UI component type (stepper, picker, secure input, etc.)
- Dirty-tracking patterns
- Edge cases (e.g., BLE PIN must be exactly 6 digits, Wi-Fi SSID max 32 UTF-8 bytes)

View File

@@ -108,17 +108,14 @@ specs/
The project constitution at `.specify/memory/constitution.md` defines non-negotiable principles.
All specs, plans, and tasks are validated against it during `/speckit.analyze`.
Current constitution (v1.2.3) enforces 9 principles:
Current constitution (v1.2.0) enforces 6 principles:
1. **KMP Core** — Business logic in `commonMain` only
2. **Zero Lint Tolerance**`spotlessCheck` + `detekt` must pass
3. **Compose Multiplatform UI** — CMP, not Android-only Compose
4. **Privacy First** — No PII/location/key exposure
5. **Design Standards Compliance** — Review against Meshtastic design standards
5. **Design Standards Compliance** — Review against Meshtastic design standards; cross-platform features must reference an upstream spec from `meshtastic/design/features/`
6. **Verify Before Push** — Local verification before any `git push`
7. **Coroutine Safety**`safeCatching` over `runCatching`, project `ioDispatcher`
8. **Resource Discipline**`stringResource(Res.string.key)`, `MeshtasticIcons`, sort-strings
9. **Branch & Scope Hygiene** — Naming conventions, upstream branching, scope limits
## Extension Hooks
@@ -132,35 +129,19 @@ Git hooks are configured in `.specify/extensions.yml` and run automatically:
### Branch Naming
Feature branches created by `/speckit.git.feature` follow sequential numbering:
`<NNN>-feature-name` (e.g., `001-local-mesh-discovery`)
Feature branches created by `/speckit.git.feature` use timestamp-based numbering:
`YYYYMMDD-HHMMSS-feature-name` (e.g., `20260511-211823-compose-screenshot-testing`)
This avoids merge conflicts when multiple specs are developed on parallel branches.
Non-spec branches follow conventional commit-style prefixes:
`feat/`, `fix/`, `chore/`, `docs/`, `build/`, `ci/`, `refactor/`, `test/`, `deps/`
### Task ID Namespacing
To avoid collision when multiple specs exist, prefix task IDs by feature:
| Spec | Prefix | Example |
|------|--------|---------|
| 001-local-mesh-discovery | `D` | D001, D002, ... |
| 002-node-list-layout | `NL-T` | NL-T001, NL-T002, ... |
| 003-app-docs-markdown | `T` | T000, T010, ... |
| 004-messaging | `MSG-T` | MSG-T001, MSG-T002, ... |
| 005-device-connections | `DC-T` | DC-T001, DC-T002, ... |
| 006-firmware-update | `FW-T` | FW-T001, FW-T002, ... |
| 007-node-detail-metrics | `NDM-T` | NDM-T001, NDM-T002, ... |
| 008-radio-app-settings | `SET-T` | SET-T001, SET-T002, ... |
| 009-map-view | `MAP-T` | MAP-T001, MAP-T002, ... |
| 010-onboarding | `OB-T` | OB-T001, OB-T002, ... |
| 011-wifi-provisioning | `WFP-T` | WFP-T001, WFP-T002, ... |
| 012-core-data | `DAT-T` | DAT-T001, DAT-T002, ... |
| 013-core-ble | `BLE-T` | BLE-T001, BLE-T002, ... |
| 014-core-network | `NET-T` | NET-T001, NET-T002, ... |
| 015-core-database | `DB-T` | DB-T001, DB-T002, ... |
| 016-core-service | `SVC-T` | SVC-T001, SVC-T002, ... |
| 017-core-model | `MDL-T` | MDL-T001, MDL-T002, ... |
To avoid collision when multiple specs exist, prefix task IDs with a short feature mnemonic
(e.g., `SST-T001` for screenshot testing, `DISC-T001` for discovery). The prefix is defined
per-spec in the tasks.md header.
### Design Standards Gate

View File

@@ -2,7 +2,7 @@
# Copied to .specify/extensions/git/git-config.yml on install
# Branch numbering strategy: "sequential" (001, 002, ...) or "timestamp" (YYYYMMDD-HHMMSS)
branch_numbering: sequential
branch_numbering: timestamp
# Commit message used by `git commit` during repository initialization
init_commit_message: "[Spec Kit] Initial commit"

View File

@@ -1 +1 @@
{"feature_directory":"specs/018-compose-screenshot-testing"}
{"feature_directory":"specs/20260511-211823-compose-screenshot-testing"}

View File

@@ -1,15 +1,18 @@
<!--
SYNC IMPACT REPORT
==================
Version change: 1.1.0 → 1.1.1
Version change: 1.1.1 → 1.2.0
Modified principles:
- Governance (compliance review wording clarified to require plan-level constitution checks)
- V. Design Standards Compliance (expanded to require cross-platform behavior spec
from meshtastic/design/features/ for multi-platform features)
Added sections: None.
Removed sections: None.
Templates requiring updates:
✅ .specify/templates/plan-template.md — Constitution Check now enumerates the six project principles.
✅ .specify/templates/spec-template.md — Validated against current constitution; no template changes required.
✅ .specify/templates/tasks-template.md — Added required verification and design review task guidance.
✅ .specify/templates/spec-template.md — Added Cross-Platform Spec metadata field and
cross-platform check guidance in Summary comment.
✅ .specify/templates/plan-template.md — Constitution Check V updated to require upstream
spec link for cross-platform features.
✅ .specify/templates/checklist-template.md — CHK005 updated to include cross-platform spec check.
Follow-up TODOs: None.
-->
@@ -77,6 +80,12 @@ All user-facing UI MUST conform to the Meshtastic Client Design Standards:
before merge.
- Deviations from the design standards require explicit justification in the PR description
with a rationale for why the standard cannot or should not be followed.
- Features that affect multiple platforms (messaging, settings, telemetry, etc.) MUST
reference an existing cross-platform behavior spec in
[`meshtastic/design/features/`](https://github.com/meshtastic/design/tree/master/features),
or create one using the `TEMPLATE.md` in that directory before writing the
Android implementation spec. Platform-specific-only features (e.g., Android widget,
Wear OS tile) may mark the `Cross-Platform Spec` field as N/A with justification.
- Rationale: Consistent cross-platform UX across Android, iOS, and other clients ensures
users have a predictable experience regardless of platform. The design standards are
maintained collaboratively across all Meshtastic client teams.
@@ -139,8 +148,13 @@ This constitution supersedes all other practices, coding guidelines, and agent i
**Amendment Procedure**:
1. Propose the amendment with rationale and a migration plan in a PR description.
2. Update `AGENTS.md` and this constitution atomically in the same commit.
3. Increment `CONSTITUTION_VERSION` per the versioning policy below.
4. All PRs and code reviews MUST verify compliance with the current constitution version.
3. Update all downstream references in the same commit:
- `.skills/speckit/SKILL.md` (principle count and descriptions)
- `.specify/templates/checklist-template.md` (checklist items)
- `.specify/templates/plan-template.md` (Constitution Check section)
- The SYNC IMPACT REPORT comment at the top of this file
4. Increment `CONSTITUTION_VERSION` per the versioning policy below.
5. All PRs and code reviews MUST verify compliance with the current constitution version.
**Versioning Policy**:
- MAJOR: Backward-incompatible principle removal or fundamental redefinition.
@@ -151,4 +165,4 @@ This constitution supersedes all other practices, coding guidelines, and agent i
Constitution Check confirming all six principles were evaluated. Complexity violations
require explicit justification in the Complexity Tracking table of the plan document.
**Version**: 1.1.1 | **Ratified**: 2026-05-07 | **Last Amended**: 2026-05-08
**Version**: 1.2.0 | **Ratified**: 2026-05-07 | **Last Amended**: 2026-05-12

View File

@@ -26,11 +26,8 @@
- [ ] CHK002 — Principle II (Zero Lint Tolerance): `spotlessApply` + `detekt` pass? [Consistency]
- [ ] CHK003 — Principle III (CMP UI): Compose Multiplatform composables? `NumberFormatter.format()` for floats? Navigation 3 patterns? [Consistency]
- [ ] CHK004 — Principle IV (Privacy First): No PII/location/key logging? Proto submodule untouched? [Consistency]
- [ ] CHK005 — Principle V (Design Standards): UI reviewed against Meshtastic design standards? [Consistency]
- [ ] CHK005 — Principle V (Design Standards): UI reviewed against Meshtastic design standards? Cross-platform features linked to upstream spec in `meshtastic/design/features/`? [Consistency]
- [ ] CHK006 — Principle VI (Verify Before Push): Full verification passing locally? [Consistency]
- [ ] CHK007 — Principle VII (Coroutine Safety): `safeCatching {}` used? Project `ioDispatcher`? [Consistency]
- [ ] CHK008 — Principle VIII (Resource Discipline): `stringResource(Res.string.key)`? `MeshtasticIcons`? `sort-strings.py` run? [Consistency]
- [ ] CHK009 — Principle IX (Branch & Scope Hygiene): Branch naming? Scope limit? [Consistency]
## [Category 1]

View File

@@ -42,7 +42,8 @@
- **IV. Privacy First**: Confirm the change does not log or expose PII, location data,
cryptographic keys, or modify the read-only `core/proto` submodule.
- **V. Design Standards Compliance**: For any user-facing UI, record how the design was
checked against the Meshtastic Client Design Standards, or explicitly mark the gate N/A.
checked against the Meshtastic Client Design Standards. For cross-platform features,
link the upstream behavior spec from `meshtastic/design/features/` or justify N/A.
- **VI. Verify Before Push**: Record the exact local verification commands and the expected
post-push CI check command (`gh pr checks` or `gh run list`) before implementation starts.

View File

@@ -3,13 +3,20 @@
**Feature Branch**: `[###-feature-name]`
**Created**: [DATE]
**Status**: Draft
**Input**: User description: "$ARGUMENTS"
**Input**: User description: "$ARGUMENTS"
**Cross-Platform Spec**: <!-- Link to meshtastic/design/features/ spec, or "N/A — platform-specific only" with justification -->
## Summary
<!--
Provide a brief (2-3 sentence) summary of the feature, its purpose, and what
user problem it solves. Mention which modules are primarily affected.
CROSS-PLATFORM CHECK: Before writing this spec, check meshtastic/design/features/
for an existing cross-platform behavior spec. If one exists, this spec should describe
the Android-specific scope and acceptance criteria — not redefine the cross-platform
behavior. If none exists and this feature affects multiple platforms, create one first
using the TEMPLATE.md in that repo.
-->
## Goals

View File

@@ -49,5 +49,5 @@ You are an expert Android/KMP engineer. Maintain architectural boundaries, use M
<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan
at `specs/018-compose-screenshot-testing/plan.md`
at `specs/20260511-211823-compose-screenshot-testing/plan.md`
<!-- SPECKIT END -->

View File

@@ -1,159 +0,0 @@
# Implementation Plan: Messaging & Contacts
**Branch**: `004-messaging` | **Date**: 2026-07-10 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/004-messaging/spec.md`
**Status**: Migrated — all implementation complete, plan reverse-engineered from existing code.
## Summary
The Messaging & Contacts feature provides the complete chat experience for Meshtastic-Android: paginated message threads with emoji reactions, reply threading, Quick Chat shortcuts, unread tracking with auto-scroll, and a paginated contact list with batch operations. Implementation uses Compose Multiplatform, Paging 3 KMP, Koin DI, and Navigation 3 — all in `commonMain` with a single `androidMain` file for WorkManager message queuing.
## Technical Context
**Language/Version**: Kotlin 2.3+ targeting JDK 21
**Primary Dependencies**: Compose Multiplatform, Material 3 Adaptive, Koin 4.2+ (K2 Compiler Plugin), Room KMP, DataStore KMP, AndroidX Paging 3 KMP, Turbine (test), Mokkery (test)
**Storage**: Room KMP for messages/contacts/quick-chat entities; DataStore KMP for UI preferences (show quick chat, emoji frequency, homoglyph encoding)
**Testing**: KMP `allTests` for `feature:messaging` — 6 test files, ~695 LOC
**Target Platform**: Android, Desktop (JVM), iOS — all via `commonMain`
**Performance Goals**: 60fps scrolling on paginated message lists; O(1) node lookup via pre-calculated map
**Constraints**: All UI in `commonMain`; no `java.*`/`android.*` in common; CMP float pre-formatting via `NumberFormatter.format()`; `safeLaunch`/`ioDispatcher` for coroutines
**Scale/Scope**: 25 commonMain files (~5,253 LOC), 1 androidMain file (~44 LOC), 6 test files (~695 LOC)
## Constitution Check
*GATE: All checks pass — existing production code reviewed against Constitution v1.2.2.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Kotlin Multiplatform Core | ✅ PASS | All logic in `commonMain`. Only `WorkManagerMessageQueue.kt` in `androidMain` uses `android.*`/`androidx.work` — correctly scoped to platform layer. |
| II. Zero Lint Tolerance | ✅ PASS | `detekt-baseline.xml` present in module. Suppression annotations used sparingly (`LongMethod`, `CyclomaticComplexMethod`, `TooManyFunctions`). |
| III. Compose Multiplatform UI | ✅ PASS | All UI uses CMP composables. `NumberFormatter.format()` used in `Contacts.kt` for mute duration. Navigation 3 `NavKey` routes in `ContactsNavigation.kt`. |
| IV. Privacy First | ✅ PASS | No PII/location/key logging. Message content not logged. Proto submodule read-only. |
| V. Design Standards Compliance | ✅ PASS | M3 components used throughout. Accessibility semantics on `MessageItem` (`a11y_message_from`). Content descriptions on all interactive icons. |
| VI. Verify Before Push | ✅ PASS | 6 test files covering ViewModels, utility functions, and composable rendering. Tests use `allTests` target. |
| VII. Coroutine Safety | ✅ PASS | All ViewModel coroutines use `safeLaunch {}` with `ioDispatcher`. No raw `runCatching {}` or `Dispatchers.IO` in common code. |
| VIII. Resource Discipline | ✅ PASS | All strings via `stringResource(Res.string.*)`. All icons from `MeshtasticIcons`. **Minor gap**: `SelectionToolbar` has 2 hardcoded English strings for mute/unmute content descriptions. |
| IX. Branch & Scope Hygiene | ✅ PASS | Feature module cleanly scoped to `feature/messaging`. DI via component scan. Routes defined in `ContactsNavigation.kt`. |
**Gate Result**: ✅ All principles satisfied (1 minor resource discipline gap noted)
## Project Structure
### Documentation (this feature)
```text
specs/004-messaging/
├── spec.md # Feature specification (migrated)
├── plan.md # This file (migrated)
└── tasks.md # Task breakdown (migrated)
```
### Source Code (repository root)
```text
feature/messaging/
├── src/commonMain/kotlin/org/meshtastic/feature/messaging/
│ ├── Message.kt ← Main MessageScreen composable + MessageInput
│ ├── MessageViewModel.kt ← ViewModel: send, react, delete, unread, filter, paging
│ ├── MessageListPaged.kt ← Paginated message list with auto-scroll + unread divider
│ ├── MessageScreenEvent.kt ← Sealed interface for UI events
│ ├── DeliveryInfoDialog.kt ← Delivery status dialog
│ ├── QuickChat.kt ← Quick Chat management screen + edit dialog
│ ├── QuickChatViewModel.kt ← Quick Chat CRUD ViewModel
│ ├── QuickChatPreviews.kt ← Preview composables for Quick Chat
│ ├── UnreadUiDefaults.kt ← Constants for unread UX behavior
│ ├── component/
│ │ ├── MessageItem.kt ← Message bubble with actions bottom sheet
│ │ ├── MessageItemPreviews.kt ← Preview composables
│ │ ├── MessageActions.kt ← Reaction + Reply + Status icon buttons
│ │ ├── MessageActionsBottomSheet.kt ← Full actions sheet (react, reply, copy, select, delete)
│ │ ├── MessageBubble.kt ← Shape logic for grouped bubbles
│ │ ├── MessageScreenComponents.kt ← Toolbar, FAB, reply snippet, delete dialog, quick chat row
│ │ ├── MessageStatusIcon.kt ← Delivery status icon composable
│ │ ├── Reaction.kt ← ReactionItem, ReactionRow, ReactionDialog
│ │ └── ReactionPreviews.kt ← Preview composables
│ ├── navigation/
│ │ └── ContactsNavigation.kt ← Navigation 3 graph (routes, entry providers)
│ ├── ui/contact/
│ │ ├── AdaptiveContactsScreen.kt ← Adaptive wrapper for contacts
│ │ ├── Contacts.kt ← ContactsScreen + selection toolbar + paged list
│ │ ├── ContactsViewModel.kt ← Contacts logic: paged, mute, delete, mark-read
│ │ └── ContactItem.kt ← Contact card composable
│ ├── ui/sharing/
│ │ └── Share.kt ← Share-to-contact screen
│ └── di/
│ └── FeatureMessagingModule.kt ← Koin module (component scan)
├── src/androidMain/kotlin/org/meshtastic/feature/messaging/worker/
│ └── WorkManagerMessageQueue.kt ← Android WorkManager message queue
└── src/commonTest/kotlin/org/meshtastic/feature/messaging/
├── MessageViewModelTest.kt ← 10 tests: init, title, connection, send, react, delete, unread
├── QuickChatViewModelTest.kt ← 3 tests: init, flow updates, add action
├── HomoglyphCharacterTransformTest.kt ← 5 tests: homoglyph encoding optimization
├── UnreadUiDefaultsTest.kt ← 1 test: default constant values
├── component/MessageItemTest.kt ← 3 tests: MQTT icon, accessibility semantics
└── ui/contact/ContactsViewModelTest.kt ← 2 tests: init, unread count flow
```
**Structure Decision**: The feature follows the standard KMP module layout. All UI and business logic in `commonMain`. The single `androidMain` file (`WorkManagerMessageQueue`) provides reliable background message sending via WorkManager — this is a legitimate platform concern that cannot be in common code.
## Module Impact
| Module | Change Type | Files Affected | Risk |
|--------|-------------|----------------|------|
| `feature/messaging` (commonMain) | Existing | 25 | Low — stable, tested |
| `feature/messaging` (androidMain) | Existing | 1 | Low — thin WorkManager wrapper |
| `feature/messaging` (commonTest) | Existing | 6 | Low — comprehensive ViewModel tests |
| `core/model` | Dependency | N/A | None — read-only usage |
| `core/repository` | Dependency | N/A | None — uses `PacketRepository`, `NodeRepository`, `ServiceRepository` |
| `core/database` | Dependency | N/A | None — uses `QuickChatAction` entity |
| `core/resources` | Dependency | N/A | None — uses string resources |
| `core/ui` | Dependency | N/A | None — uses shared components (`MeshtasticDialog`, `NodeChip`, `AutoLinkText`, etc.) |
## Integration Points
- **Navigation**: Routes defined in `ContactsNavigation.kt` using Navigation 3 `NavKey` typed routes (`ContactsRoute.ContactsGraph`, `.Messages`, `.Share`, `.QuickChat`). Integrated with `ListDetailSceneStrategy` for adaptive layouts.
- **DI**: `FeatureMessagingModule` uses Koin `@ComponentScan` to auto-discover `@KoinViewModel`-annotated ViewModels.
- **Repositories**: `PacketRepository` (messages, contacts, unread counts), `NodeRepository` (node info), `ServiceRepository` (connection state, service actions), `RadioConfigRepository` (channels), `QuickChatActionRepository` (quick chat CRUD).
- **Preferences**: `UiPrefs` (show quick chat), `CustomEmojiPrefs` (emoji frequency), `HomoglyphPrefs` (encoding toggle).
- **Notifications**: `NotificationManager.cancel()` called when all unread messages are cleared.
## Design Constraints
- All UI lives in `commonMain` — not platform-specific
- Strings accessed via `stringResource(Res.string.key)` — never hardcoded
- Icons use `MeshtasticIcons` exclusively (from `core/ui/icon/`)
- Error handling uses `safeCatching {}` not `runCatching {}`
- Dispatchers via `org.meshtastic.core.common.util.ioDispatcher`
- Float values must be pre-formatted with `NumberFormatter.format()` (CMP constraint)
- Paging uses AndroidX Paging 3 KMP with `cachedIn(viewModelScope)`
- Message byte limit is 200 bytes (UTF-8 encoded) — enforced at UI level
## Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| Hardcoded strings in SelectionToolbar | Low | Low | Fix mute/unmute `contentDescription` to use `stringResource()` |
| Missing composable tests for ContactItem, ShareScreen | Low | Medium | Add UI tests for untested composables |
| Pagination edge case with Select All | Low | Low | Documented limitation — only selects loaded items |
## Phase Alignment with Tasks
| Phase | Purpose | Key Tasks | Dependencies |
|-------|---------|-----------|--------------|
| 1. Data Layer | Message/contact repositories + DI | MSG-T001MSG-T003 | None |
| 2. Message Thread | Core chat screen + message list | MSG-T004MSG-T014 | Phase 1 |
| 3. Contacts | Contact list + management | MSG-T015MSG-T022 | Phase 1 |
| 4. Polish & Test | Quick chat, share, tests | MSG-T023MSG-T030 | Phases 23 |
### Critical Path
```
Phase 1 (Data) → Phase 2 (Messages) → Phase 3 (Contacts) → Phase 4 (Polish)
```
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| *None* | — | — |

View File

@@ -1,242 +0,0 @@
# Feature Specification: Messaging & Contacts
**Feature Branch**: `004-messaging`
**Created**: 2026-07-10
**Status**: Migrated
**Input**: Brownfield migration — reverse-engineered from existing `feature/messaging` module
## Summary
Messaging & Contacts is the primary communication feature of Meshtastic-Android. It provides a full-featured chat experience over the Meshtastic mesh network, including paginated message threads, contact/channel management, emoji reactions, reply threading, quick-chat shortcuts, unread tracking, message filtering, mute/notification controls, and a share-to-contact flow. All business logic and Compose UI reside in `commonMain`, with a single `androidMain` file for WorkManager-based background message queuing.
## Goals
1. Enable users to send and receive text messages over the Meshtastic mesh, to both individual nodes (DMs) and broadcast channels.
2. Provide a contact list that surfaces all conversations, with unread counts, mute controls, and multi-select batch operations.
3. Support emoji reactions on messages with delivery status tracking.
4. Offer Quick Chat shortcuts for common messages (instant send or append to input).
5. Deliver paginated, performant message lists with unread divider, auto-scroll, and scroll-to-bottom FAB.
## Non-Goals
- End-to-end encryption management — handled by the radio firmware and `core/proto`.
- File or image attachments — only text messages are supported.
- Push notifications routing — handled by `core/service` and `core/repository`.
- Group chat creation or channel provisioning — managed by `feature/settings` and channel config.
## User Scenarios & Testing *(mandatory)*
### User Story 1 — Send a Text Message (Priority: P1)
A user opens a conversation, types a message, and sends it over the mesh. The message appears in the thread with a delivery status icon that updates as the mesh acknowledges it.
**Why this priority**: Core messaging is the fundamental capability of the app — all other features depend on it.
**Independent Test**: Send a message to a connected node; verify the message appears locally with QUEUED → ENROUTE → RECEIVED status progression.
**Acceptance Scenarios**:
1. **Given** a connected device and an open conversation, **When** the user types "Hello" and taps Send, **Then** the message appears in the thread with status QUEUED and the input clears.
2. **Given** a message with status ENROUTE, **When** the remote node acknowledges receipt, **Then** the status icon updates to RECEIVED.
3. **Given** a message exceeding 200 bytes, **When** the user types, **Then** the byte counter turns red, the send button is disabled, and the message cannot be sent.
4. **Given** the device is disconnected, **When** the user views the message input, **Then** the input field is disabled and the send button is non-interactive.
---
### User Story 2 — View Contact List & Navigate to Conversations (Priority: P1)
A user sees a paginated list of all conversations (DM contacts + broadcast channels). Channel placeholders appear even when no messages exist yet. Tapping a contact opens its message thread.
**Why this priority**: The contact list is the entry point for all messaging interactions.
**Independent Test**: Open the app with existing conversations; verify contacts display with last message preview, time, and unread badges.
**Acceptance Scenarios**:
1. **Given** three conversations with messages, **When** the contacts screen loads, **Then** all three appear sorted by most recent message.
2. **Given** a contact with 5 unread messages, **When** viewing the contacts list, **Then** a badge shows "5" next to the contact.
3. **Given** two configured channels with no messages, **When** viewing contacts, **Then** placeholder entries for each channel appear.
4. **Given** the contacts list, **When** the user taps a contact, **Then** the app navigates to the message thread for that contact.
---
### User Story 3 — Unread Message Tracking & Auto-Scroll (Priority: P1)
When a user opens a conversation with unread messages, the list scrolls to the first unread message and shows an "Unread Messages" divider. As the user scrolls through messages, they are marked as read after a debounce period.
**Why this priority**: Unread tracking is critical UX for a messaging app — users must know what's new.
**Independent Test**: Receive messages while the app is backgrounded; reopen the thread and verify the divider appears and read-marking works.
**Acceptance Scenarios**:
1. **Given** a conversation with 10 unread messages, **When** the user opens the thread, **Then** the list scrolls to the first unread message with 5 context messages visible above the divider.
2. **Given** the user is reading messages and stops scrolling for 500ms, **When** unread messages are visible, **Then** those messages are marked as read and the notification is cleared if all are read.
3. **Given** new messages arrive while the user is at the bottom and no unread divider is present, **When** a new message appears, **Then** the list auto-scrolls to show it.
---
### User Story 4 — Emoji Reactions (Priority: P2)
A user can react to any message with an emoji. Reactions appear below the message bubble, grouped by emoji, with a count. Users can view reaction details in a dialog.
**Why this priority**: Reactions add expressiveness without consuming mesh bandwidth for full messages.
**Independent Test**: Long-press a message, select an emoji reaction, and verify it appears below the bubble.
**Acceptance Scenarios**:
1. **Given** a received message, **When** the user long-presses and selects 👍, **Then** a 👍 reaction appears below the message bubble.
2. **Given** two users react with the same emoji, **When** viewing the message, **Then** the reaction shows the emoji with count "2".
3. **Given** a user already reacted with 👍, **When** the user taps 👍 again, **Then** the duplicate reaction is prevented (no re-send).
4. **Given** a conversation with reactions, **When** the user long-presses a reaction, **Then** a dialog shows who reacted, timestamps, and SNR/RSSI metadata.
---
### User Story 5 — Quick Chat Shortcuts (Priority: P2)
Users can create, reorder, and use Quick Chat actions — short pre-configured messages that either send instantly or append to the current input.
**Why this priority**: Quick Chat enables fast communication on constrained mesh networks (low bandwidth, slow typing).
**Independent Test**: Create a Quick Chat action, toggle Instant mode, use it during a conversation.
**Acceptance Scenarios**:
1. **Given** the Quick Chat panel is visible, **When** the user taps an Instant action, **Then** its message is sent immediately.
2. **Given** the Quick Chat panel is visible, **When** the user taps an Append action, **Then** its text is appended to the input field.
3. **Given** the Quick Chat options screen, **When** the user drags an action to a new position, **Then** the order is persisted and reflected in the chat panel.
---
### User Story 6 — Contact Management (Mute, Delete, Multi-Select) (Priority: P2)
Users can long-press contacts to enter selection mode. Selected contacts can be batch-deleted, muted for a duration, or unmuted. A "Mark All as Read" action clears all unread badges.
**Why this priority**: Contact management keeps the conversation list organized, especially on busy meshes.
**Independent Test**: Long-press a contact, select multiple, mute them, verify the mute icon appears and persists.
**Acceptance Scenarios**:
1. **Given** a contact list, **When** the user long-presses a contact, **Then** selection mode activates with a toolbar showing count, delete, mute, and select-all actions.
2. **Given** two selected contacts, **When** the user chooses "Mute for 1 week", **Then** both contacts show a mute icon and notifications are suppressed for 7 days.
3. **Given** selected contacts with messages, **When** the user taps delete and confirms, **Then** the contacts and all associated messages are removed.
4. **Given** 3 conversations with unread messages, **When** the user taps "Mark All as Read" in the app bar, **Then** all unread badges clear to 0.
---
### Edge Cases
- What happens when a message reply references a message not yet loaded in the paged list? → The app attempts to scroll to it; if not found in the snapshot, no scroll occurs (graceful no-op).
- How does the system handle the BEL character (`\u0007`) in messages? → A 🔔 icon is displayed and the message bubble gets a red border to indicate an alert/bell message.
- What happens when "Select All" is used with pagination? → Only currently loaded items are selected; a full-list select is not possible with paging.
- How are filtered messages handled? → Messages can be filtered per-contact; filtered messages appear at reduced alpha (0.5) with a "Filtered" label. Users can toggle filter visibility per-contact.
- What happens when message byte size approaches the 200-byte limit with multi-byte characters? → The byte counter correctly counts UTF-8 bytes (not characters), preventing oversized packets.
## Architecture
### Key Components
| Component | Module / File | Purpose |
|-----------|---------------|---------|
| `MessageScreen` | `feature/messaging/Message.kt` | Main chat screen composable with input, top bar, quick chat |
| `MessageViewModel` | `feature/messaging/MessageViewModel.kt` | Business logic: send, react, delete, unread tracking, paging |
| `MessageListPaged` | `feature/messaging/MessageListPaged.kt` | Paginated lazy list with auto-scroll, unread divider |
| `MessageItem` | `feature/messaging/component/MessageItem.kt` | Individual message bubble with reactions, status, reply snippet |
| `MessageActionsContent` | `feature/messaging/component/MessageActionsBottomSheet.kt` | Bottom sheet with quick emojis, reply, copy, select, delete |
| `MessageBubble` | `feature/messaging/component/MessageBubble.kt` | Shape logic for grouped message bubbles |
| `Reaction*` | `feature/messaging/component/Reaction.kt` | Reaction item, row, dialog composables |
| `MessageScreenComponents` | `feature/messaging/component/MessageScreenComponents.kt` | Toolbar, FAB, reply snippet, delete dialog, quick chat row, utility functions |
| `MessageStatusIcon` | `feature/messaging/component/MessageStatusIcon.kt` | Animated delivery status icon |
| `ContactsScreen` | `feature/messaging/ui/contact/Contacts.kt` | Paginated contacts list with selection, mute, delete |
| `ContactsViewModel` | `feature/messaging/ui/contact/ContactsViewModel.kt` | Contact list logic: paged contacts, mute, delete, mark-read |
| `ContactItem` | `feature/messaging/ui/contact/ContactItem.kt` | Individual contact card with unread badge, mute icon |
| `AdaptiveContactsScreen` | `feature/messaging/ui/contact/AdaptiveContactsScreen.kt` | Navigation wrapper for contacts |
| `ShareScreen` | `feature/messaging/ui/sharing/Share.kt` | Contact picker for message sharing |
| `QuickChatScreen` | `feature/messaging/QuickChat.kt` | Quick chat management with drag-to-reorder |
| `QuickChatViewModel` | `feature/messaging/QuickChatViewModel.kt` | CRUD for quick chat actions |
| `ContactsNavigation` | `feature/messaging/navigation/ContactsNavigation.kt` | Navigation 3 route definitions |
| `FeatureMessagingModule` | `feature/messaging/di/FeatureMessagingModule.kt` | Koin DI module (component scan) |
| `WorkManagerMessageQueue` | `feature/messaging/worker/WorkManagerMessageQueue.kt` | Android-only WorkManager message queuing |
| `UnreadUiDefaults` | `feature/messaging/UnreadUiDefaults.kt` | Shared constants for unread UX behavior |
| `MessageScreenEvent` | `feature/messaging/MessageScreenEvent.kt` | Sealed interface for UI events |
| `DeliveryInfoDialog` | `feature/messaging/DeliveryInfoDialog.kt` | Message delivery status dialog |
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST send text messages to individual nodes (DM) and broadcast channels.
- **FR-002**: System MUST display messages in a reverse-chronological paginated list with sender identification.
- **FR-003**: System MUST show delivery status icons for locally-sent messages (QUEUED, ENROUTE, RECEIVED, ERROR, etc.).
- **FR-004**: System MUST enforce a 200-byte message limit with real-time byte counter display.
- **FR-005**: System MUST support emoji reactions on messages with deduplication (prevent re-sending same reaction).
- **FR-006**: System MUST track unread messages per contact and display counts as badges.
- **FR-007**: System MUST show an "Unread Messages" divider and scroll to first unread on conversation open.
- **FR-008**: System MUST auto-scroll to new messages when the user is at/near the bottom of the list.
- **FR-009**: System MUST mark messages as read after a 500ms scroll debounce and clear notifications when all are read.
- **FR-010**: System MUST support reply threading — replying to a specific message and displaying the original as a snippet.
- **FR-011**: System MUST provide Quick Chat actions (Instant and Append modes) with drag-to-reorder.
- **FR-012**: System MUST display a paginated contact list with last message preview, time, and unread badge.
- **FR-013**: System MUST support multi-select contact operations: delete, mute (8h/1week/always), unmute, select all.
- **FR-014**: System MUST show channel placeholder contacts for all configured channels even without messages.
- **FR-015**: System MUST support message selection mode with copy-to-clipboard, delete, and select-all actions.
- **FR-016**: System MUST allow message resend from the status dialog when status is ERROR.
- **FR-017**: System MUST support homoglyph encoding optimization for Cyrillic text to reduce byte usage.
- **FR-018**: System MUST provide a Share screen for forwarding messages to a selected contact.
### Non-Functional Requirements
- **NFR-001**: Message list MUST use Paging 3 (`LazyPagingItems`) for memory-efficient rendering of large conversations.
- **NFR-002**: Node lookup in message list MUST be O(1) via pre-calculated `nodeMap` (not O(N) per item).
- **NFR-003**: List item animations MUST be disabled during scroll to prevent jank/stutter.
- **NFR-004**: All string resources MUST use `stringResource(Res.string.*)` — no hardcoded user-facing text in `commonMain`.
- **NFR-005**: All icons MUST use `MeshtasticIcons.*` exclusively.
- **NFR-006**: Coroutines MUST use `safeLaunch` (not raw `launch`) and `ioDispatcher` (not `Dispatchers.IO`).
## Source-Set Impact
| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | 25 files — all logic and UI | Business logic + CMP composables per Constitution §I, §III |
| `androidMain` | 1 file — `WorkManagerMessageQueue.kt` | Platform-specific WorkManager integration for reliable background message send |
| `commonTest` | 6 files — ViewModel and component tests | KMP test coverage per Constitution §VI |
## Design Standards Compliance
- [x] New screens reviewed against design standards (existing code, production-validated)
- [x] M3 component selection verified — `OutlinedTextField`, `Scaffold`, `TopAppBar`, `Card`, `ListItem`, `ModalBottomSheet`, `FloatingActionButton`, `AssistChip`, `Badge`
- [x] Accessibility: TalkBack semantics on `MessageItem` (`a11y_message_from`), content descriptions on all icons, haptic feedback on long-press
- [x] Typography: M3 type scale used throughout (`bodyLarge`, `labelSmall`, `titleMedium`, etc.)
## Privacy Assessment
- [x] No PII, location data, or cryptographic keys logged or exposed
- [x] No new network calls that transmit user data (messages routed through existing service layer)
- [x] Proto submodule (`core/proto`) not modified (read-only upstream)
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Users can send a text message and see it delivered to a connected mesh node.
- **SC-002**: Message delivery status updates correctly through QUEUED → ENROUTE → RECEIVED lifecycle.
- **SC-003**: Unread count badges on the contacts list accurately reflect unread messages per conversation.
- **SC-004**: Opening a conversation with unreads scrolls to the first unread message with the divider visible.
- **SC-005**: Emoji reactions are delivered, displayed, grouped, and deduplicated correctly.
- **SC-006**: Quick Chat actions (Instant and Append) work correctly and persist reordering.
- **SC-007**: Contact multi-select operations (delete, mute, unmute) apply correctly to all selected contacts.
- **SC-008**: Message list scrolls at 60fps with no visible jank during paged loading of large conversations.
## Assumptions
- All business logic and UI composables reside in `commonMain` source set.
- String resources added to `core/resources/src/commonMain/composeResources/values/strings.xml`.
- Icons use `MeshtasticIcons` (from `core/ui/icon/`).
- Float values pre-formatted with `NumberFormatter.format()` (CMP constraint).
- Message routing and service communication handled by `core/repository` and `core/service`.
- Paging provided by AndroidX Paging 3 KMP (`androidx.paging`).
- Navigation uses Navigation 3 typed `NavKey` routes via `core/navigation`.
- DI uses Koin 4.2+ with K2 Compiler Plugin component scanning.

View File

@@ -1,252 +0,0 @@
# Tasks: Messaging & Contacts
**Spec**: [spec.md](spec.md) | **Plan**: [plan.md](plan.md)
**Status**: Migrated — all existing tasks marked complete. Gap tasks marked incomplete.
**Task Prefix**: `MSG-T`
---
## Phase 1 — Data Layer & DI
### MSG-T001: Koin DI module setup [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/di/FeatureMessagingModule.kt`
- Created `FeatureMessagingModule` with `@ComponentScan` for auto-discovery of `@KoinViewModel` classes.
- **Test**: Module loads without error in app startup.
### MSG-T002: MessageViewModel — core state and repository wiring [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt`
- Injects `NodeRepository`, `RadioConfigRepository`, `PacketRepository`, `ServiceRepository`, `QuickChatActionRepository`, `UiPrefs`, `CustomEmojiPrefs`, `HomoglyphPrefs`, `NotificationManager`, `SendMessageUseCase`.
- Exposes `nodeList`, `ourNodeInfo`, `connectionState`, `channels`, `showQuickChat`, `quickChatActions`, `contactSettings`, `frequentEmojis`, `homoglyphEncodingEnabled`.
- Uses `SavedStateHandle` for `contactKey` initialization.
- **Test**: `MessageViewModelTest.kt` — 10 tests covering init, title, connection state, send, react, delete, unread count, clear unread, node integration.
### MSG-T003: ContactsViewModel — contact list and management [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.kt`
- Provides both non-paginated (`contactList`) and paginated (`contactListPaged`) contact flows.
- Supports `deleteContacts`, `markAllAsRead`, `setMuteUntil`, `setContactFilteringDisabled`, `getTotalMessageCount`.
- **Test**: `ContactsViewModelTest.kt` — 2 tests covering init, unread count total flow.
---
## Phase 2 — Message Thread Screen
### MSG-T004: MessageScreenEvent sealed interface [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageScreenEvent.kt`
- Defines events: `SendMessage`, `SendReaction`, `DeleteMessages`, `ClearUnreadCount`, `NodeDetails`, `SetTitle`, `NavigateToNodeDetails`, `NavigateBack`, `CopyToClipboard`.
### MSG-T005: MessageScreen composable [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt`
- Main `Scaffold` with top bar (normal + action mode), message list, quick chat row, reply snippet, message input.
- Handles contact key parsing, channel resolution, mismatch key detection.
- Manages unread scroll logic: initial scroll to first unread with 5-message context.
### MSG-T006: MessageInput composable [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt` (private composable)
- `OutlinedTextField` with byte counter, 200-byte limit enforcement, send-on-Enter (desktop keyboard), multi-line (max 3), homoglyph encoding support.
- Disabled state when device is disconnected.
- Preview composables for normal, disabled, over-limit, and multi-byte character states.
### MSG-T007: MessageListPaged composable [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt`
- Reverse-layout `LazyColumn` with `LazyPagingItems`.
- Groups consecutive messages from same sender (bubble shape optimization).
- Unread divider placement based on `firstUnreadMessageUuid`.
- Status dialog for ERROR messages with resend option.
- Reaction dialog showing who reacted, metadata (SNR/RSSI/hops), and delivery status.
- Animation disabled during scroll for performance.
### MSG-T008: Auto-scroll and unread tracking [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt`
- `AutoScrollToBottomPaged`: Caches "at bottom" state when scroll is idle to prevent stuttering. Scrolls to item 0 on new message if cached position is at bottom.
- `UpdateUnreadCountPaged`: Uses `snapshotFlow` + 500ms `debounce` to mark visible unread messages as read after scroll settles. Lifecycle-aware (pauses on background).
### MSG-T009: UnreadUiDefaults constants [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/UnreadUiDefaults.kt`
- `VISIBLE_CONTEXT_COUNT = 5`, `AUTO_SCROLL_BOTTOM_OFFSET_TOLERANCE = 8`, `SCROLL_DEBOUNCE_MILLIS = 500L`.
- **Test**: `UnreadUiDefaultsTest.kt` — 1 test validating constant values.
### MSG-T010: MessageItem composable [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt`
- Node chip, sender name, auto-link text, SNR/RSSI/hops metadata, transport icon, timestamp.
- BEL character detection (red border + 🔔 icon).
- Filtered message alpha + label.
- `ModalBottomSheet` with `MessageActionsContent` or `EmojiPickerDialog`.
- Original message reply snippet with clickable navigation.
- Accessibility: merged semantics with `a11y_message_from` content description.
- **Test**: `MessageItemTest.kt` — 3 tests: MQTT icon visibility, semantic content description.
### MSG-T011: MessageBubble shape logic [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageBubble.kt`
- `getMessageBubbleShape()` returns corner-based shape based on sender direction and grouping (hasSamePrev/hasSameNext).
### MSG-T012: Message actions and status icons [x]
- **Files**:
- `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActions.kt`
- `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt`
- `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageStatusIcon.kt`
- `MessageActions` row: reaction button + reply button + status button.
- `MessageActionsContent` bottom sheet: quick emoji row (6 + "more"), reply, copy, select, delete, status.
- `MessageStatusIcon`: Maps `MessageStatus` enum to `MeshtasticIcons.*` vectors.
### MSG-T013: Reaction composables [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt`
- `ReactionItem`: Single emoji with count, sending/error states (alpha, error container).
- `ReactionRow`: Grouped emoji row with add-reaction button.
- `ReactionDialog`: Bottom sheet showing who reacted, timestamps, SNR/RSSI/hops, status dialog for own reactions.
- `AddReactionButton`: Opens emoji picker dialog.
### MSG-T014: Message screen toolbar components [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageScreenComponents.kt`
- `MessageTopBar`: Title + security icon + PKC key status + overflow menu.
- `ActionModeTopBar`: Selection count + copy/delete/select-all actions.
- `ScrollToBottomFab`: FAB with unread badge.
- `ReplySnippet`: Animated reply-to bar with original message snippet (50 char limit).
- `DeleteMessageDialog`: Confirmation with plural string.
- `QuickChatRow`: Horizontal action buttons with 🔔 alert action prepended.
- `handleQuickChatAction()`: Instant (send) vs. Append (add to input) with byte-limit enforcement.
- `UnreadMessagesDivider`: Styled horizontal divider with "New messages" label.
- `MessageStatusDialog`: Delivery info dialog with resend option.
- Utility functions: `ellipsize()`, `limitBytes()`.
---
## Phase 3 — Contacts Screen
### MSG-T015: ContactItem composable [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactItem.kt`
- `Card` with `AssistChip` (node short name with colors), long name, last message time, last message text preview.
- Unread count badge (capped at 99+), mute icon, security icon for broadcast channels.
- Selected/active/outlined card states.
### MSG-T016: ContactsScreen with paginated list [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt`
- `Scaffold` with `MainAppBar` showing "Mark All as Read" when unreads exist.
- Channel placeholder generation for empty channels.
- Paginated `LazyColumn` with `LazyPagingItems`.
- Loading indicator for append state.
- `MeshtasticImportFAB` for sharing/importing channels when connected.
### MSG-T017: Contact selection mode [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt`
- Long-press enters selection mode. Toolbar shows count, close, mute/unmute, delete, select-all.
- `DeleteConfirmationDialog` with plural strings.
- `MuteNotificationsDialog` with radio options: unmute, 8 hours, 1 week, always. Shows current mute status per contact.
### MSG-T018: AdaptiveContactsScreen wrapper [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/AdaptiveContactsScreen.kt`
- Bridges `ContactsScreen` with Navigation 3 `NavBackStack` for adaptive (list-detail) layout.
### MSG-T019: ContactsNavigation graph [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/navigation/ContactsNavigation.kt`
- Defines `entryProviderScope` with routes:
- `ContactsRoute.ContactsGraph` / `ContactsRoute.Contacts``ContactsEntryContent` (list pane)
- `ContactsRoute.Messages(contactKey)``MessageScreen` (detail pane)
- `ContactsRoute.Share(message)``ShareScreen` (extra pane)
- `ContactsRoute.QuickChat``QuickChatScreen` (extra pane)
- Uses `ListDetailSceneStrategy` for adaptive panes.
- `dropUnlessResumed` for safe navigation callbacks.
---
## Phase 4 — Quick Chat, Share, Polish & Tests
### MSG-T020: QuickChatViewModel [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChatViewModel.kt`
- CRUD operations: `addQuickChatAction`, `deleteQuickChatAction`, `updateActionPositions`.
- **Test**: `QuickChatViewModelTest.kt` — 3 tests: init, flow reflection, add action delegation.
### MSG-T021: QuickChatScreen with drag-to-reorder [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChat.kt`
- `LazyColumn` with `dragDropItemsIndexed` for reordering.
- `EditQuickChatDialog`: name (5 chars), message (200 bytes), Instant/Append toggle, delete option.
- FAB to add new action.
- Auto-generates short name from message text.
### MSG-T022: ShareScreen [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/sharing/Share.kt`
- Contact picker using non-paginated `contactList` from `ContactsViewModel`.
- Single-select with send button. Navigates to `Messages` route with pre-filled message.
### MSG-T023: DeliveryInfoDialog [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/DeliveryInfoDialog.kt`
- Generic delivery info dialog with title, text, relay count (pluralized), optional resend button.
### MSG-T024: Homoglyph encoding support [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt` (MessageInput)
- When `homoglyphEncodingEnabled`, applies `HomoglyphCharacterStringTransformer.optimizeUtf8StringWithHomoglyphs()` to reduce byte usage for Cyrillic text.
- **Test**: `HomoglyphCharacterTransformTest.kt` — 5 tests: shrink with homoglyphs, half-size all-homoglyphs, no transform for non-homoglyphs, no transform for Latin, no transform for Arabic.
### MSG-T025: WorkManagerMessageQueue (Android) [x]
- **File**: `feature/messaging/src/androidMain/kotlin/org/meshtastic/feature/messaging/worker/WorkManagerMessageQueue.kt`
- Android-only `MessageQueue` implementation using `OneTimeWorkRequestBuilder<SendMessageWorker>`.
- Uses `ExistingWorkPolicy.REPLACE` with unique work name per packet.
### MSG-T026: MessageViewModel tests [x]
- **File**: `feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/MessageViewModelTest.kt`
- 10 tests: initialization, set title, connection state, toggle quick chat, frequent emojis, send message, send reaction, delete messages, unread count, clear unread count, node repository integration.
- Uses Mokkery mocks, Turbine for flow testing, `StandardTestDispatcher`.
### MSG-T027: QuickChatViewModel tests [x]
- **File**: `feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/QuickChatViewModelTest.kt`
- 3 tests: initialization, actions flow reflects repo updates, add action delegates to repo.
### MSG-T028: MessageItem UI tests [x]
- **File**: `feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/component/MessageItemTest.kt`
- 3 tests: MQTT icon displayed when `viaMqtt=true`, MQTT icon absent when `viaMqtt=false`, correct semantic `contentDescription`.
- Uses `runComposeUiTest` from `androidx.compose.ui.test.v2`.
---
## Gap Tasks (Identified During Migration)
### MSG-T029: Fix hardcoded English strings in SelectionToolbar [x]
- **File**: `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/Contacts.kt` (lines 468470)
- **Issue**: `contentDescription` for mute/unmute icons uses hardcoded `"Mute selected"` / `"Unmute selected"` instead of `stringResource()`.
- **Constitution**: Violates Principle VIII (Resource Discipline).
- **Fix**: Add `mute_selected` and `unmute_selected` string resources; replace hardcoded strings with `stringResource(Res.string.mute_selected)` / `stringResource(Res.string.unmute_selected)`.
### MSG-T030: Add missing composable tests [ ]
- **Gap**: No tests for `ContactItem`, `ShareScreen`, `ReactionRow`, `ReactionDialog`, `QuickChatRow`, `MessageBubble` composables.
- **Recommended**: Add `ContactItemTest.kt` covering unread badge display, mute icon, selection states. Add `ShareScreenTest.kt` covering contact selection and send flow. Add `ReactionRowTest.kt` covering emoji grouping and add-reaction button.
- **Priority**: Low — existing ViewModel tests cover core logic; composable tests are incremental improvement.
---
## Summary
| Status | Count | Description |
|--------|-------|-------------|
| ✅ Completed | 28 | All existing implementation tasks |
| ⬜ Gap | 2 | 1 resource discipline fix, 1 test coverage gap |
| **Total** | **30** | |

View File

@@ -1,172 +0,0 @@
# Implementation Plan: Device Connections
**Branch**: `005-device-connections` | **Date**: 2026-07-14 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/005-device-connections/spec.md`
**Status**: Migrated — reverse-engineered from existing `feature/connections` module
## Summary
Device Connections provides BLE scanning, USB/Serial enumeration, TCP/NSD network discovery, manual IP entry, and device selection/disconnection from a unified Connections screen. The implementation follows a platform-subclass pattern: `ScannerViewModel` in `commonMain` handles scan state, device lists, and selection logic; `AndroidScannerViewModel` and `JvmScannerViewModel` override bonding/permission flows. All UI is Compose Multiplatform in `commonMain`. Device discovery is delegated to `GetDiscoveredDevicesUseCase` with platform-specific implementations.
## Technical Context
**Language/Version**: Kotlin 2.3+ targeting JDK 21
**Primary Dependencies**: Compose Multiplatform, Material 3 Adaptive, Koin 4.2+ (K2 Compiler Plugin), DataStore KMP, Navigation 3
**Storage**: DataStore KMP for preferences (`UiPrefs`: auto-scan, transport visibility); `RecentAddressesDataSource` for recent TCP addresses
**Testing**: KMP `allTests` for `feature:connections` — 3 test files, 26 tests (Turbine + Mokkery + Kotest matchers)
**Target Platform**: Android, Desktop (JVM) — all via `commonMain`
**Performance Goals**: BLE scan results within 1 scan interval; RSSI throttled to 2s; RSSI read timeout 1s
**Constraints**: All UI in `commonMain`; no `java.*`/`android.*` in common; CMP float pre-formatting via `NumberFormatter.format()`
**Scale/Scope**: 20 commonMain files, 3 androidMain files, 4 jvmMain files, 3 commonTest files
## Constitution Check
*GATE: All principles verified against existing implementation.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Kotlin Multiplatform Core | ✅ PASS | All business logic and UI in `commonMain`. Platform code limited to `androidMain` (bonding, USB permission) and `jvmMain` (direct GATT connect, JVM USB stub). No `java.*`/`android.*` in common. |
| II. Zero Lint Tolerance | ✅ PASS | `detekt-baseline.xml` present. Suppressions documented (`LongParameterList`, `TooManyFunctions`, `CyclomaticComplexMethod`). |
| III. Compose Multiplatform UI | ✅ PASS | CMP composables throughout. Navigation 3 via `connectionsGraph()`. `stringResource(Res.string.*)` for all labels. |
| IV. Privacy First | ✅ PASS | Device addresses anonymized via `anonymize()` in all log output. No PII logged. Proto submodule read-only. NSD is local-only. |
| V. Design Standards Compliance | ✅ PASS | M3 components: `FilterChip`, `OutlinedButton`, `Card`, `ListItem`, `ModalBottomSheet`. Accessibility: `selectable`, `Role.RadioButton`, `combinedClickable` with `onClickLabel`. |
| VI. Verify Before Push | ✅ PASS | 26 tests pass via `allTests`. `spotlessApply` + `detekt` required before merge. |
| VII. Coroutine Safety | ✅ PASS | `safeLaunch` used for all coroutine launches. `safeCatchingAll` in use case. Project `CoroutineDispatchers` injected (not `Dispatchers.IO`). |
| VIII. Resource Discipline | ✅ PASS | `stringResource(Res.string.*)` for all UI text. `MeshtasticIcons` for all icons. |
| IX. Branch & Scope Hygiene | ✅ PASS | Module scoped to `feature/connections`. Clean separation of concerns across source sets. |
**Gate Result**: ✅ All principles satisfied
## Project Structure
### Documentation (this feature)
```text
specs/005-device-connections/
├── spec.md # Feature specification (migrated)
├── plan.md # This file (migrated)
└── tasks.md # Task list (migrated)
```
### Source Code (repository root)
```text
feature/connections/
├── build.gradle.kts
├── detekt-baseline.xml
├── src/commonMain/kotlin/org/meshtastic/feature/connections/
│ ├── Constants.kt ← Address prefixes: NO_DEVICE_SELECTED, TCP, BLE, MOCK
│ ├── ScannerViewModel.kt ← Platform-neutral ViewModel: scan, select, disconnect
│ ├── di/
│ │ └── FeatureConnectionsModule.kt ← Koin @Module + @ComponentScan
│ ├── domain/usecase/
│ │ ├── CommonGetDiscoveredDevicesUseCase.kt ← Platform-agnostic TCP + USB + mock aggregation
│ │ ├── TcpDiscoveryHelpers.kt ← Shared: processTcpServices, matchNodes, buildRecent
│ │ └── UsbScanner.kt ← Interface for platform USB enumeration
│ ├── model/
│ │ ├── DeviceListEntry.kt ← Sealed class: Ble, Usb, Tcp, Mock
│ │ └── DiscoveredDevices.kt ← Data class + GetDiscoveredDevicesUseCase interface
│ ├── navigation/
│ │ └── ConnectionsNavigation.kt ← Navigation 3: connectionsGraph()
│ └── ui/
│ ├── ConnectionsScreen.kt ← Top-level screen: status card + list + filter chips
│ └── components/
│ ├── ConnectingDeviceInfo.kt ← Connecting state card
│ ├── ConnectionActionButton.kt ← Shared icon+label button (4 styles)
│ ├── ConnectionActionButtonStyle.kt ← Enum: Filled, Tonal, Outlined, Text
│ ├── CurrentlyConnectedInfo.kt ← Connected card: battery, RSSI polling, node chip
│ ├── DeviceList.kt ← LazyColumn: BLE/Network/USB sections + AddDialog
│ ├── DeviceListItem.kt ← Device row: icon, name, RSSI, radio button
│ ├── DeviceSectionHeader.kt ← Section header with progress + trailing action
│ ├── DisconnectButton.kt ← Error-tinted OutlinedButton
│ ├── EmptyStateContent.kt ← Full-page empty state (unused — inline variant in DeviceList)
│ └── TransportFilterChips.kt ← BLE/Network/USB filter chips
├── src/androidMain/kotlin/org/meshtastic/feature/connections/
│ ├── AndroidScannerViewModel.kt ← createBond() + USB permission flow
│ ├── domain/usecase/
│ │ └── AndroidGetDiscoveredDevicesUseCase.kt ← Bonded BLE + USB serial + TCP
│ └── model/
│ └── AndroidUsbDeviceData.kt ← Wraps UsbSerialDriver
├── src/jvmMain/kotlin/org/meshtastic/feature/connections/
│ ├── JvmScannerViewModel.kt ← Direct GATT connect (no explicit bonding)
│ ├── domain/usecase/
│ │ ├── JvmGetDiscoveredDevicesUseCase.kt ← Wraps CommonGetDiscoveredDevicesUseCase
│ │ └── JvmUsbScanner.kt ← Stub (empty list)
│ └── model/
│ └── JvmUsbDeviceData.kt ← Stub UsbDeviceData
└── src/commonTest/kotlin/org/meshtastic/feature/connections/
├── ScannerViewModelTest.kt ← 11 tests: scan state, device selection, NSD gating, sort order
├── domain/usecase/
│ ├── CommonGetDiscoveredDevicesUseCaseTest.kt ← 10 tests: TCP discovery, node matching, mock
│ └── TcpDiscoveryHelpersTest.kt ← 10 tests: processTcpServices, matchNodes, buildRecent, findByNameSuffix
```
**Structure Decision**: Feature module follows the standard KMP pattern. Platform-specific ViewModel subclasses are in `androidMain`/`jvmMain` and bound via Koin `@KoinViewModel(binds = [...])`. Use case interface is in `commonMain`; implementations are platform-specific `@Single` bindings.
## Module Impact
| Module | Change Type | Files Affected | Risk |
|--------|-------------|----------------|------|
| `feature/connections` (commonMain) | Full feature | 20 files | Low — self-contained |
| `feature/connections` (androidMain) | Platform impl | 3 files | Medium — OS bonding/permissions |
| `feature/connections` (jvmMain) | Platform stubs | 4 files | Low — thin wrappers |
| `feature/connections` (commonTest) | Tests | 3 files | Low |
| `core/ble` | Dependency | 0 (consumed) | Low — read-only |
| `core/network` | Dependency | 0 (consumed) | Low — read-only |
| `core/datastore` | Dependency | 0 (consumed) | Low — read-only |
| `core/resources` | Modify | strings.xml entries | Low |
## Integration Points
- **Navigation**: `ConnectionsRoute.Connections` and `ConnectionsRoute.ConnectionsGraph` registered via `connectionsGraph()` in `ConnectionsNavigation.kt`. Uses Navigation 3 `entry<>` pattern.
- **DI**: `FeatureConnectionsModule` with `@ComponentScan("org.meshtastic.feature.connections")`. Android binds `AndroidScannerViewModel``ScannerViewModel` via `@KoinViewModel(binds = [...])`. Android binds `AndroidGetDiscoveredDevicesUseCase``GetDiscoveredDevicesUseCase` via `@Single(binds = [...])`.
- **DataStore Keys**: `UiPrefs.bleAutoScan`, `UiPrefs.networkAutoScan`, `UiPrefs.showBleTransport`, `UiPrefs.showNetworkTransport`, `UiPrefs.showUsbTransport`.
- **Radio Controller**: `RadioController.setDeviceAddress()` for device selection/disconnection.
- **Service Repository**: `ServiceRepository.connectionProgress` flow for status chatter; `ServiceRepository.setErrorMessage()` for bonding failures.
- **Settings Integration**: Imports `RadioConfigViewModel` and `ConfigRoute.LORA` for the "Set your region" flow. Depends on `feature/settings` module.
## Design Constraints
- All UI lives in `commonMain` — not platform-specific
- Strings accessed via `stringResource(Res.string.key)` — never hardcoded
- Icons use `MeshtasticIcons` exclusively (from `core/ui/icon/`)
- Error handling uses `safeCatching {}` / `safeCatchingAll {}` not `runCatching {}`
- Dispatchers via injected `CoroutineDispatchers` — not `Dispatchers.IO`
- Float values must be pre-formatted with `NumberFormatter.format()` (CMP constraint)
- RSSI polling throttled to 2-second intervals with 1-second read timeout
- NSD scanning gated behind user toggle to avoid Android 15+ system consent on screen entry
## Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| BLE bonding flakiness on some Android OEMs | Medium | Medium | `requestBonding()` catches all exceptions; known "bond state 11" handled as non-error |
| Android 15+ NSD consent dialog disrupts UX | Low | Low | NSD gated behind explicit user toggle; `ACCESS_LOCAL_NETWORK` requested via launcher |
| RSSI read timeout blocking UI | Low | Medium | `withTimeout(1.seconds)` + `TimeoutCancellationException` caught gracefully |
| USB permission denial | Low | Low | Permission flow surfaces denial via log; user can re-tap to retry |
## Phase Alignment with Tasks
| Phase | Purpose | Key Tasks | Dependencies |
|-------|---------|-----------|--------------|
| 1. Setup | Constants, DI, build config | DC-T001DC-T004 | None |
| 2. Models & Domain | Data models, use cases, helpers | DC-T005DC-T011 | Phase 1 |
| 3. US1 — BLE Discovery | ViewModel + BLE scan + device list | DC-T012DC-T016 | Phase 2 |
| 4. US2/US3 — TCP/Network | NSD discovery + manual add | DC-T017DC-T019 | Phase 2 |
| 5. US4 — USB/Serial | USB enumeration + permission | DC-T020DC-T021 | Phase 2 |
| 6. US5 — Connection Status | Status card states + disconnect | DC-T022DC-T025 | Phase 3 |
| 7. US6 — Transport Filters | Filter chips + persistence | DC-T026DC-T027 | Phase 3 |
| 8. Tests & Verification | All test files + lint | DC-T028DC-T032 | All prior |
### Critical Path
```
Phase 1 → Phase 2 → Phase 3 (BLE/ViewModel) → Phase 6 (Status) → Phase 8 (Tests)
```
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| *None* | — | — |

View File

@@ -1,251 +0,0 @@
# Feature Specification: Device Connections
**Feature Branch**: `005-device-connections`
**Created**: 2026-07-14
**Status**: Migrated
**Input**: Brownfield migration — reverse-engineered from existing `feature/connections` module
## Summary
Device Connections is the central connection management feature of Meshtastic-Android. It provides BLE scanning, USB/Serial enumeration, TCP/NSD (mDNS) network discovery, manual IP entry, and device selection/disconnection — all from a unified Connections screen. The feature drives a `ScannerViewModel` with platform subclasses (`AndroidScannerViewModel`, `JvmScannerViewModel`) that handle bonding, permissions, and transport-specific pairing. All business logic and Compose Multiplatform UI reside in `commonMain`; platform-specific bonding and USB permission flows are delegated to `androidMain` and `jvmMain` source sets.
## Goals
1. Allow users to discover nearby Meshtastic nodes via BLE, USB, and TCP/NSD and connect to them with a single tap.
2. Provide transport-visibility filter chips so users can hide irrelevant transports (e.g., hide USB on a phone with no OTG cable).
3. Support manual TCP address entry (IP + port) for direct connections without mDNS.
4. Display real-time connection status, progress chatter, and RSSI signal quality for BLE devices.
5. Persist recent TCP addresses and BLE auto-scan / network auto-scan preferences across sessions.
## Non-Goals
- Firmware update or OTA — handled by a separate feature module.
- Channel or radio configuration — managed by `feature/settings`.
- Bluetooth permissions prompts — handled by `core/ble` and the OS; this feature assumes permissions are already granted.
- Mesh topology display or route management — handled by `feature/nodes`.
## User Scenarios & Testing *(mandatory)*
### User Story 1 — Discover and Connect via BLE (Priority: P1)
A user opens the Connections screen and taps "Scan Bluetooth" to discover nearby Meshtastic devices. They see a list of bonded and scanned BLE devices, each showing the device name, MAC address, signal strength (RSSI), and (if previously connected) a node chip with the mesh identity. Tapping a bonded device immediately initiates a connection; tapping an unbonded device triggers the OS bonding dialog first.
**Why this priority**: BLE is the primary transport for the majority of Meshtastic users. Without BLE discovery, users cannot connect to their radios.
**Independent Test**: Can be tested by starting a BLE scan, verifying devices appear in the list with correct RSSI indicators, and tapping a device to connect.
**Acceptance Scenarios**:
1. **Given** the Connections screen is open and BLE auto-scan is enabled, **When** the screen opens, **Then** BLE scanning starts automatically and the scanning indicator is visible.
2. **Given** a BLE scan is running, **When** a Meshtastic device is discovered, **Then** it appears in the BLE section with its advertised name and RSSI.
3. **Given** a bonded BLE device is in the list, **When** the user taps it, **Then** the connection is initiated immediately and the status card shows "Connecting…".
4. **Given** an unbonded BLE device is in the list, **When** the user taps it, **Then** the platform bonding dialog is shown; on success, the connection proceeds.
5. **Given** a BLE scan is running, **When** the user taps "Stop", **Then** scanning stops but discovered devices remain visible.
6. **Given** multiple devices are discovered, **When** the list renders, **Then** bonded devices sort by name first, then unbonded devices appear in discovery order (RSSI updates do not reorder).
---
### User Story 2 — Discover and Connect via TCP/Network (Priority: P2)
A user enables network scanning to discover Meshtastic devices via NSD/mDNS on the local network. Discovered devices show their short name and device ID derived from TXT records. Previously connected TCP devices appear in a "Recent" section for quick reconnection.
**Why this priority**: TCP is the second most common transport, especially for users connecting to stationary nodes or using the device over Wi-Fi.
**Independent Test**: Can be tested by enabling network scan, verifying NSD-discovered devices appear, and tapping one to connect.
**Acceptance Scenarios**:
1. **Given** the user taps "Scan Network", **When** NSD discovery resolves services, **Then** discovered TCP devices appear with display names derived from mDNS TXT records (`shortname` + `id`).
2. **Given** a device was previously connected via TCP, **When** the Connections screen opens, **Then** it appears in the "Recent Network Devices" section (unless currently discovered via NSD).
3. **Given** a discovered TCP device exists in the local node database, **When** it renders, **Then** a NodeChip with the mesh identity is shown.
4. **Given** the user long-presses a recent TCP device, **When** the context action fires, **Then** it can be removed from the recent list.
5. **Given** Android 15+ requires `ACCESS_LOCAL_NETWORK` for NSD, **When** permission is not yet granted, **Then** the system permission dialog is shown before scanning starts.
---
### User Story 3 — Add a Manual TCP Device (Priority: P2)
A user taps "Add network device manually" and enters an IP address and optional port in a bottom sheet dialog. The device is added to the recent list and a connection is initiated immediately.
**Why this priority**: Not all networks support mDNS; manual entry is essential for advanced users and enterprise deployments.
**Independent Test**: Can be tested by opening the manual-add dialog, entering a valid IP, and verifying the device is added and selected.
**Acceptance Scenarios**:
1. **Given** the user taps "Add network device manually", **When** the bottom sheet opens, **Then** an address field and a port field (defaulting to `4403`) are shown.
2. **Given** the user enters a valid IP address, **When** they tap "Add", **Then** the device is added to recent addresses and selected as the active device.
3. **Given** the user enters an invalid address, **When** they tap "Add", **Then** nothing happens (validation prevents submission).
---
### User Story 4 — Connect via USB/Serial (Priority: P3)
A user plugs in a Meshtastic device via USB. The device appears in the USB section of the Connections list. On Android, the USB permission dialog is shown if not already granted.
**Why this priority**: USB is a less common transport but critical for firmware development and desktop use.
**Independent Test**: Can be tested by connecting a USB device and verifying it appears in the list; tapping grants permission and connects.
**Acceptance Scenarios**:
1. **Given** a USB device is connected, **When** the Connections screen is open, **Then** the device appears in the USB section with its device name and serial path.
2. **Given** USB permission has not been granted, **When** the user taps the device, **Then** the Android USB permission dialog is shown; on approval, connection proceeds.
3. **Given** a demo/mock transport is enabled, **When** the device list renders, **Then** a "Demo Mode" entry appears in the USB section.
---
### User Story 5 — View Connection Status and Disconnect (Priority: P1)
A user can see the current connection state at the top of the Connections screen. When connected, the status card shows the node's battery level, firmware version, signal strength, and a disconnect button. When connecting, it shows a progress spinner and status text. When no device is selected, it shows an empty state.
**Why this priority**: Connection status visibility is essential for all users to know whether their radio is accessible.
**Independent Test**: Can be tested by connecting a device and verifying the status card transitions correctly between NO_DEVICE → CONNECTING → CONNECTED states.
**Acceptance Scenarios**:
1. **Given** no device is selected, **When** the screen renders, **Then** the card shows "No device selected" with a muted icon.
2. **Given** a device is selected but not yet connected, **When** the screen renders, **Then** the card shows the device name, address, and a "Connecting…" spinner with progress text.
3. **Given** a device is connected and node info is available, **When** the screen renders, **Then** the card shows the node chip, battery info, RSSI (for BLE), firmware version, and a disconnect button.
4. **Given** a device is connected, **When** the user taps "Disconnect", **Then** the device address is cleared, the persisted device name is reset, and the card transitions to the "No device selected" state.
5. **Given** a device is connected and the LoRa region is not set, **When** the status card renders, **Then** a "Set your region" warning card is shown below.
---
### User Story 6 — Filter Transport Sections (Priority: P3)
A user toggles transport filter chips (BLE, Network, USB) to show or hide sections in the device list. Preferences are persisted across sessions.
**Why this priority**: UX refinement — reduces clutter when users only use one transport.
**Independent Test**: Can be tested by toggling each chip and verifying the corresponding section appears or disappears.
**Acceptance Scenarios**:
1. **Given** all transport chips are selected, **When** the device list renders, **Then** BLE, Network, and USB sections are all visible.
2. **Given** the user deselects the BLE chip, **When** the list re-renders, **Then** the BLE section is hidden.
3. **Given** the user re-opens the Connections screen, **When** preferences were persisted, **Then** the same transport filter state is restored.
---
### Edge Cases
- What happens when BLE scanning starts but no devices are found? → Section shows inline empty-state hint: "No Bluetooth devices seen — try scanning."
- What happens when the user disconnects mid-bonding? → `requestBonding()` catches exceptions and surfaces error via `serviceRepository.setErrorMessage()`.
- What happens when NSD resolves a device already in the recent list? → The device appears only in the Discovered section; it is filtered out of the Recent section.
- What happens when a TCP address has a non-default port? → The port is appended to the address string (e.g., `192.168.1.50:5000`).
- What happens when RSSI updates arrive during a scan? → RSSI is updated on the card but does not trigger a re-sort of the device list.
## Architecture
### Key Components
| Component | Module / File | Purpose |
|-----------|---------------|---------|
| `ScannerViewModel` | `feature/connections/ScannerViewModel.kt` | Platform-neutral ViewModel: BLE/USB/TCP scan control, device selection, disconnect |
| `AndroidScannerViewModel` | `feature/connections/AndroidScannerViewModel.kt` | Android override: `createBond()` for BLE, USB permission via `UsbRepository` |
| `JvmScannerViewModel` | `feature/connections/JvmScannerViewModel.kt` | JVM/Desktop override: direct GATT connect without explicit bonding |
| `ConnectionsScreen` | `feature/connections/ui/ConnectionsScreen.kt` | Top-level Composable: animated status card + device list + transport chips |
| `DeviceList` | `feature/connections/ui/components/DeviceList.kt` | Unified `LazyColumn` with BLE/Network/USB sections and manual-add dialog |
| `DeviceListItem` | `feature/connections/ui/components/DeviceListItem.kt` | Individual device row: icon, headline, address, RSSI, radio button |
| `TransportFilterChips` | `feature/connections/ui/components/TransportFilterChips.kt` | BLE/Network/USB filter chip row |
| `CurrentlyConnectedInfo` | `feature/connections/ui/components/CurrentlyConnectedInfo.kt` | Connected state card: battery, RSSI polling, node chip, firmware version |
| `ConnectingDeviceInfo` | `feature/connections/ui/components/ConnectingDeviceInfo.kt` | Connecting state card: spinner, status label, disconnect button |
| `DeviceListEntry` | `feature/connections/model/DeviceListEntry.kt` | Sealed class: `Ble`, `Usb`, `Tcp`, `Mock` device entries |
| `DiscoveredDevices` | `feature/connections/model/DiscoveredDevices.kt` | Data class aggregating all discovered device lists |
| `CommonGetDiscoveredDevicesUseCase` | `feature/connections/domain/usecase/CommonGetDiscoveredDevicesUseCase.kt` | Platform-agnostic device aggregation: TCP + USB + mock |
| `AndroidGetDiscoveredDevicesUseCase` | `feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt` | Android-specific: adds bonded BLE + USB serial devices |
| `TcpDiscoveryHelpers` | `feature/connections/domain/usecase/TcpDiscoveryHelpers.kt` | Shared helpers: `processTcpServices`, `matchDiscoveredTcpNodes`, `buildRecentTcpEntries` |
| `UsbScanner` | `feature/connections/domain/usecase/UsbScanner.kt` | Interface for platform-specific USB device enumeration |
| `ConnectionsNavigation` | `feature/connections/navigation/ConnectionsNavigation.kt` | Navigation 3 graph: `ConnectionsRoute.Connections` and `ConnectionsRoute.ConnectionsGraph` |
| `FeatureConnectionsModule` | `feature/connections/di/FeatureConnectionsModule.kt` | Koin DI module with `@ComponentScan` |
### Data Flow
```mermaid
graph TD
A[ConnectionsScreen] --> B[ScannerViewModel]
A --> C[ConnectionsViewModel]
B --> D[BleScanner.scan]
B --> E[GetDiscoveredDevicesUseCase]
B --> F[RadioController.setDeviceAddress]
E --> G[NodeRepository.nodeDBbyNum]
E --> H[RecentAddressesDataSource]
E --> I[NetworkRepository.resolvedList]
E --> J[UsbScanner.scanUsbDevices]
B --> K[UiPrefs - auto-scan, transport visibility]
```
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST discover nearby BLE devices advertising the Meshtastic service UUID and display them with name, address, and RSSI.
- **FR-002**: System MUST distinguish bonded from unbonded BLE devices and route unbonded devices through the platform bonding flow before connecting.
- **FR-003**: System MUST discover TCP/Network devices via NSD/mDNS and display them with short name and device ID derived from TXT records.
- **FR-004**: System MUST gate NSD scanning behind a user-initiated toggle to avoid Android 15+ system consent dialogs on screen entry.
- **FR-005**: System MUST allow manual TCP device addition by IP address and optional port (default 4403).
- **FR-006**: System MUST persist recent TCP addresses via `RecentAddressesDataSource` and display them in a "Recent" section when not currently discovered via NSD.
- **FR-007**: System MUST enumerate connected USB/Serial devices and display them in the USB section with device name and serial path.
- **FR-008**: System MUST show the current connection status in three states: NO_DEVICE, CONNECTING (with progress chatter), and CONNECTED (with node info, battery, firmware version).
- **FR-009**: System MUST allow the user to disconnect the current device, clearing the persisted device address and name.
- **FR-010**: System MUST provide transport filter chips (BLE, Network, USB) that toggle section visibility, with preferences persisted via `UiPrefs`.
- **FR-011**: System MUST sort bonded BLE devices by name and unbonded scanned devices by discovery order; RSSI updates MUST NOT trigger re-sorting.
- **FR-012**: System MUST match discovered/recent devices to nodes in the local database (by device ID or MAC suffix) and display a `NodeChip` when matched.
- **FR-013**: System MUST display a "Set your region" warning when the connected device's LoRa region is unset (unless in mock/demo mode).
- **FR-014**: System MUST provide a mock/demo transport entry in the USB section when the mock transport is enabled.
### Non-Functional Requirements
- **NFR-001**: BLE scan results MUST appear in the list within 1 scan interval of advertisement reception.
- **NFR-002**: RSSI display on device cards MUST throttle updates to every 2 seconds to prevent excessive recomposition.
- **NFR-003**: Connected device RSSI polling MUST timeout after 1 second per read to avoid blocking the UI.
- **NFR-004**: All UI composables MUST reside in `commonMain` — no `android.*` imports in UI code.
- **NFR-005**: Device addresses MUST be anonymized in log output via `anonymize()` extension to protect user privacy.
## Source-Set Impact
| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | 20 files / ~2,496 lines | All business logic, UI, models, use cases, navigation |
| `commonTest` | 3 files / ~760 lines | ViewModel tests, use case tests, TCP helper tests |
| `androidMain` | 3 files / ~293 lines | `AndroidScannerViewModel` (bonding), `AndroidGetDiscoveredDevicesUseCase` (BLE + USB), `AndroidUsbDeviceData` |
| `jvmMain` | 4 files / ~174 lines | `JvmScannerViewModel`, `JvmGetDiscoveredDevicesUseCase`, `JvmUsbScanner`, `JvmUsbDeviceData` |
## Design Standards Compliance
- [x] New screens reviewed against [design standards](https://raw.githubusercontent.com/meshtastic/design/refs/heads/master/standards/meshtastic_design_standards_latest.md)
- [x] M3 component selection verified (e.g., `FilterChip` for transports, `OutlinedButton` for disconnect, `Card` for device rows)
- [x] Accessibility: TalkBack semantics (`selectable`, `selected`, `Role.RadioButton`), combined-clickable with `onClickLabel`, content descriptions for transport icons
- [x] Typography: `titleSmall` for section headers, `headlineSmall` for device name, `bodyLarge` for addresses, `labelLarge` for status
## Privacy Assessment
- [x] No PII, location data, or cryptographic keys logged or exposed — device addresses anonymized via `anonymize()`
- [x] No new network calls that transmit user data — NSD discovery is local-only
- [x] Proto submodule (`core/proto`) not modified (read-only upstream)
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: User can discover and connect to a BLE device in ≤3 taps from the Connections screen.
- **SC-002**: NSD-discovered TCP devices display correct short name and device ID within 5 seconds of network scan start.
- **SC-003**: Recent TCP addresses persist across app restarts and are restored on the Connections screen.
- **SC-004**: Transport filter chip state persists across screen navigation and app restarts.
- **SC-005**: Connection state transitions (NO_DEVICE → CONNECTING → CONNECTED) render with animated crossfade within 1 frame.
- **SC-006**: All 3 test files (26 tests total) pass in `commonTest` via `allTests`.
- **SC-007**: BLE device list remains stable (no reordering) when RSSI updates arrive.
## Assumptions
- All business logic and UI composables reside in `commonMain` source set.
- String resources added to `core/resources/src/commonMain/composeResources/values/strings.xml`.
- Icons use `MeshtasticIcons` (from `core/ui/icon/`).
- Float values pre-formatted with `NumberFormatter.format()` (CMP constraint).
- `BleScanner` is injected as nullable — platforms without BLE (JVM desktop) pass `null`.
- `UsbScanner` is injected as nullable — platforms without USB enumeration pass `null`.
- `ACCESS_LOCAL_NETWORK` runtime permission (Android 15+) is handled at the `ConnectionsScreen` Composable level via `rememberRequestLocalNetworkPermission`.
- The `ConnectionsViewModel` (from `core/ui`) provides `connectionState`, `connectionStatus`, `ourNodeForDisplay`, and `regionUnset` — this feature does not own those flows.

View File

@@ -1,211 +0,0 @@
# Tasks: Device Connections
**Input**: Design documents from `/specs/005-device-connections/`
**Prerequisites**: plan.md (required), spec.md (required for user stories)
**Status**: Migrated — all implemented tasks marked `[x]`; gap tasks marked `[ ]`
## Format: `[ID] [P?] [Story?] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
- Include exact file paths in descriptions
## Path Conventions
- **KMP commonMain**: `feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/`
- **androidMain**: `feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/`
- **jvmMain**: `feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/`
- **Tests (KMP)**: `feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/`
- **Resources**: `core/resources/src/commonMain/composeResources/values/strings.xml`
---
## Phase 1: Setup (Shared Infrastructure)
**Purpose**: Build configuration, constants, DI module, and navigation registration.
- [x] DC-T001 [P] Configure `build.gradle.kts` with `meshtastic.kmp.feature` plugin, `commonMain` dependencies (`core:common`, `core:data`, `core:database`, `core:datastore`, `core:di`, `core:domain`, `core:model`, `core:navigation`, `core:prefs`, `core:proto`, `core:resources`, `core:service`, `core:ui`, `core:ble`, `core:network`, `feature:settings`), and `androidMain` dependency (`usb-serial-android`).
- [x] DC-T002 [P] Create `Constants.kt` — define `NO_DEVICE_SELECTED`, `TCP_DEVICE_PREFIX`, `MOCK_DEVICE_PREFIX`, `BLE_DEVICE_PREFIX` sentinel constants.
- [x] DC-T003 [P] Create `di/FeatureConnectionsModule.kt` — Koin `@Module` with `@ComponentScan("org.meshtastic.feature.connections")`.
- [x] DC-T004 [P] Create `navigation/ConnectionsNavigation.kt` — Navigation 3 `connectionsGraph()` with entries for `ConnectionsRoute.ConnectionsGraph` and `ConnectionsRoute.Connections`.
**Dependencies**: None.
**Checkpoint**: Module builds, DI wired, navigation registered.
---
## Phase 2: Models & Domain Logic
**Purpose**: Data models, use case interfaces, platform implementations, and TCP discovery helpers.
- [x] DC-T005 [P] [US1/US2/US4] Create `model/DeviceListEntry.kt` — sealed class with `Ble`, `Usb`, `Tcp`, `Mock` subtypes. Include `UsbDeviceData` interface, `fullAddress`/`address` properties, `getMeshtasticShortName()` extension.
- [x] DC-T006 [P] [US1/US2/US4] Create `model/DiscoveredDevices.kt` — data class aggregating `bleDevices`, `usbDevices`, `discoveredTcpDevices`, `recentTcpDevices`. Define `GetDiscoveredDevicesUseCase` interface.
- [x] DC-T007 [P] [US4] Create `domain/usecase/UsbScanner.kt` — interface for platform USB device enumeration.
- [x] DC-T008 [P] [US2] Create `domain/usecase/TcpDiscoveryHelpers.kt` — shared helpers: `processTcpServices()`, `matchDiscoveredTcpNodes()`, `buildRecentTcpEntries()`, `findNodeByNameSuffix()`.
- [x] DC-T009 [US2/US4] Create `domain/usecase/CommonGetDiscoveredDevicesUseCase.kt` — platform-agnostic implementation combining TCP, USB, and mock devices via `combine()`. Not `@Single` annotated (platform overrides provide canonical binding).
- [x] DC-T010 [US1/US4] Create `androidMain/.../AndroidGetDiscoveredDevicesUseCase.kt` — Android-specific: bonded BLE filtering via `BluetoothRepository.state`, USB via `UsbRepository.serialDevices`, node matching by MAC suffix. `@Single(binds = [GetDiscoveredDevicesUseCase::class])`.
- [x] DC-T011 [P] Create `jvmMain/.../JvmGetDiscoveredDevicesUseCase.kt`, `JvmUsbScanner.kt`, `JvmUsbDeviceData.kt` — JVM stubs wrapping `CommonGetDiscoveredDevicesUseCase`.
**Dependencies**: Phase 1 must complete first.
**Checkpoint**: All data models and use cases ready — ViewModel and UI can begin.
---
## Phase 3: User Story 1 — BLE Discovery & Connection (Priority: P1) 🎯 MVP
**Goal**: Users can scan for BLE devices, see bonded + unbonded list sorted by name/discovery-order, and connect with a single tap.
**Independent Test**: Start BLE scan → verify devices appear with RSSI → tap bonded device → connection initiates.
### Implementation
- [x] DC-T012 [US1] Create `ScannerViewModel.kt` — platform-neutral ViewModel: BLE scan (`startBleScan`/`stopBleScan`/`toggleBleScan`), `scannedBleDevices` map, `discoveryOrder` list, `bleDevicesForUi` StateFlow combining bonded + scanned with stable sort, `onSelected()` routing, `changeDeviceAddress()`, `disconnect()`, connection progress text, mock transport toggle.
- [x] DC-T013 [US1] Create `androidMain/.../AndroidScannerViewModel.kt` — Android override: `requestBonding()` via `bluetoothRepository.bond()` with `SecurityException` + generic exception handling + "bond state 11" special case. `requestPermission()` via `usbRepository.requestPermission()`. `@KoinViewModel(binds = [ScannerViewModel::class])`.
- [x] DC-T014 [P] [US1] Create `jvmMain/.../JvmScannerViewModel.kt` — JVM override: direct GATT connect without explicit bonding.
- [x] DC-T015 [US1] Create `ui/components/DeviceListItem.kt` — device row composable: transport-appropriate icon (`Bluetooth`/`BluetoothConnected`/`BluetoothSearching`/`Usb`/`Wifi`/`Add`), `NodeChip` headline when node matched, throttled RSSI display (2s interval), `RadioButton` trailing content, `selectable`/`combinedClickable` with `Role.RadioButton` + `onClickLabel`.
- [x] DC-T016 [US1] Create `ui/components/DeviceList.kt` — unified `LazyColumn` with `bluetoothSection()`: `DeviceSectionHeader` with scan toggle, `DeviceCard` items with `animateItem()`, inline empty state (`SectionEmptyState`).
**Dependencies**: Phase 2 must complete first.
**Checkpoint**: BLE discovery and connection works end-to-end.
---
## Phase 4: User Story 2/3 — TCP/Network Discovery & Manual Add (Priority: P2)
**Goal**: Users can discover TCP devices via NSD/mDNS, see recent TCP addresses, and manually add devices by IP.
**Independent Test**: Enable network scan → verify NSD devices appear → add manual IP → device selected.
### Implementation
- [x] DC-T017 [US2] Extend `DeviceList.kt` `networkSection()` — discovered TCP items, recent TCP sub-section via `recentNetworkSection()`, "Add network device manually" tonal button, scan toggle in header.
- [x] DC-T018 [US3] Implement `AddDeviceDialog` in `DeviceList.kt``ModalBottomSheet` with address (`OutlinedTextField`, `KeyboardType.Uri`) + port (`KeyboardType.Decimal`, default `4403`) fields. Validation via `isValidAddress()`. Non-default port appended as `address:port`.
- [x] DC-T019 [US2] Implement NSD gating in `ScannerViewModel.kt``_isNetworkScanning` flag, `gatedResolvedList` via `flatMapLatest`, `toggleNetworkScan()` + `persistNetworkAutoScanIntent()`. Android 15+ `ACCESS_LOCAL_NETWORK` handled in `ConnectionsScreen.kt` via `rememberRequestLocalNetworkPermission`.
**Dependencies**: Phase 2 use cases + Phase 3 DeviceList scaffold.
**Checkpoint**: TCP discovery (NSD + recent + manual) works end-to-end.
---
## Phase 5: User Story 4 — USB/Serial Connection (Priority: P3)
**Goal**: Users can see connected USB devices and connect with permission grant.
**Independent Test**: Plug in USB device → verify it appears in USB section → tap to connect.
### Implementation
- [x] DC-T020 [US4] Extend `DeviceList.kt` `usbSection()` — USB device items with section header, inline empty state.
- [x] DC-T021 [P] [US4] Create `androidMain/.../model/AndroidUsbDeviceData.kt` — wrapper for `UsbSerialDriver` implementing `UsbDeviceData` interface.
**Dependencies**: Phase 2 use cases.
**Checkpoint**: USB enumeration and permission-gated connection works.
---
## Phase 6: User Story 5 — Connection Status & Disconnect (Priority: P1)
**Goal**: Users see animated connection state card (NO_DEVICE → CONNECTING → CONNECTED) with node info, battery, RSSI, firmware, and disconnect button.
**Independent Test**: Connect device → verify card transitions → tap disconnect → card resets.
### Implementation
- [x] DC-T022 [US5] Create `ui/ConnectionsScreen.kt` — top-level Composable: `Scaffold` with `MainAppBar`, `AdaptiveTwoPane`, `AnimatedContent` with `fadeIn togetherWith fadeOut` for 3 states (`ConnectionUiState` enum). Auto-start BLE scan via `LaunchedEffect(bleAutoScan)`, auto-start NSD via `DisposableEffect`. Region warning card below status card.
- [x] DC-T023 [US5] Create `ui/components/CurrentlyConnectedInfo.kt` — connected card: `MaterialBatteryInfo`, `Rssi` with polling loop (`withTimeout(1.seconds)`, `delay(2.seconds)`), `NodeChip` with click-to-navigate, firmware version text, `DisconnectButton`.
- [x] DC-T024 [US5] Create `ui/components/ConnectingDeviceInfo.kt` — connecting card: `CircularProgressIndicator`, device name + address, status label from `ConnectionStatus` enum with progress text fallback, `DisconnectButton`.
- [x] DC-T025 [P] [US5] Create `ui/components/DisconnectButton.kt` — full-width `OutlinedButton` with `error` color tint.
**Dependencies**: Phase 3 (ViewModel wired).
**Checkpoint**: Connection lifecycle UI works end-to-end.
---
## Phase 7: User Story 6 — Transport Filter Chips (Priority: P3)
**Goal**: Users can toggle BLE/Network/USB section visibility via filter chips; preferences persist.
**Independent Test**: Toggle each chip → verify section hides/shows → restart → verify state restored.
### Implementation
- [x] DC-T026 [US6] Create `ui/components/TransportFilterChips.kt``FilterChip` row for BLE, Network, USB with `MeshtasticIcons` leading icons. Wired to `UiPrefs.showBleTransport` / `showNetworkTransport` / `showUsbTransport`.
- [x] DC-T027 [US6] Wire filter chips in `ConnectionsScreen.kt` — read state from `ScannerViewModel`, pass toggles to `TransportFilterChips`, gate `DeviceList` sections on `showBleSection` / `showNetworkSection` / `showUsbSection`.
**Dependencies**: Phase 3 (DeviceList + ViewModel).
**Checkpoint**: Transport filtering with persistence works.
---
## Phase 8: Tests, Shared Components & Verification
**Purpose**: Unit tests, shared UI components, lint, and final verification.
### Shared Components
- [x] DC-T028 [P] Create `ui/components/ConnectionActionButton.kt` — shared icon+label button with 4 styles (Filled, Tonal, Outlined, Text) + `ConnectionActionButtonStyle.kt` enum. Used by scan toggles, add-device button.
- [x] DC-T029 [P] Create `ui/components/DeviceSectionHeader.kt` — section header with `titleSmall` label, optional `LinearProgressIndicator`, trailing composable slot.
- [x] DC-T030 [P] Create `ui/components/EmptyStateContent.kt` — full-page empty state composable (icon + text + optional action).
### Tests (Implemented)
- [x] DC-T031 Write `ScannerViewModelTest.kt` — 11 tests covering: initialization, connection progress updates, BLE scan start/stop, device address change, USB device updates, network scan toggle, NSD gating (empty when not scanning, populates when active), BLE sort order (bonded-first then discovery-order, RSSI no-reorder), stop-scan preserves discovered list.
- [x] DC-T032 Write `CommonGetDiscoveredDevicesUseCaseTest.kt` — 10 tests covering: empty state, recent address sort, mock toggle, node matching by suffix, no-match without database, reactive node updates, discovered TCP from NSD, discovered TCP node matching, empty resolved list, mock in empty state.
- [x] DC-T033 Write `TcpDiscoveryHelpersTest.kt` — 10 tests covering: `processTcpServices` (shortname+id, default name, recent name priority, no-duplicate-id, sort order), `matchDiscoveredTcpNodes` (node match, no-database), `buildRecentTcpEntries` (filter discovered, suffix match, sort), `findNodeByNameSuffix` (no-database, match, short-suffix rejection).
### Verification
- [x] DC-T034 Run `./gradlew :feature:connections:allTests` — 26 tests pass.
- [x] DC-T035 Run `./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests` — green.
### Gap Tasks (Not Yet Implemented) ⚠️
- [ ] **[DEFERRED]** DC-T036 `[GAP]` [US1] Write `AndroidScannerViewModelTest` in `feature/connections/src/androidTest/` — test `requestBonding()` success/failure paths, `requestPermission()` USB flow, `SecurityException` handling, "bond state 11" special case. *Rationale: Android-specific bonding and permission logic has no test coverage.**Deferred: requires Android instrumented test (androidTest) for bonding/permission APIs.*
- [ ] **[DEFERRED]** DC-T037 `[GAP]` [US1/US5] Write Compose UI tests for `ConnectionsScreen` in `feature/connections/src/commonTest/` — test `AnimatedContent` state transitions (NO_DEVICE → CONNECTING → CONNECTED), transport chip toggles, device card selection. *Rationale: All existing tests are ViewModel/use-case level; no UI-layer test coverage.**Deferred: requires Compose UI test infrastructure.*
- [x] DC-T038 `[GAP]` Add KDoc to `ConnectionActionButtonStyle.kt` — document each enum value (`Filled`, `Tonal`, `Outlined`, `Text`) with usage context. *Rationale: Only enum in the module without documentation.*
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies
- **Models & Domain (Phase 2)**: Depends on Phase 1
- **US1 — BLE (Phase 3)**: Depends on Phase 2 — **critical path**
- **US2/US3 — TCP (Phase 4)**: Depends on Phase 2 + Phase 3 scaffold
- **US4 — USB (Phase 5)**: Depends on Phase 2
- **US5 — Status (Phase 6)**: Depends on Phase 3
- **US6 — Filters (Phase 7)**: Depends on Phase 3
- **Tests (Phase 8)**: Depends on all prior phases
### Critical Path
```
Phase 1 → Phase 2 → Phase 3 (BLE/ViewModel) → Phase 6 (Status) → Phase 8 (Tests)
```
### Parallel Opportunities
```
Phase 4 (TCP) ∥ Phase 5 (USB) ∥ Phase 7 (Filters) — all depend on Phase 2/3 but are independent of each other
DC-T028/T029/T030 (shared components) — independent, parallelizable
```
---
## Implementation Strategy
### Status: Complete (Migrated)
All 35 implementation tasks are complete. 3 gap tasks identified for future work:
1. **DC-T036**: Android-specific ViewModel tests (bonding/permissions)
2. **DC-T037**: Compose UI tests (screen state transitions)
3. **DC-T038**: KDoc for `ConnectionActionButtonStyle`
### Recommended Follow-Up
- Use `/speckit.specify` to create a follow-up spec for gap tasks
- Use `/speckit.bugfix.report` if bonding edge cases surface in production

View File

@@ -1,191 +0,0 @@
# Implementation Plan: Firmware Update (OTA / DFU)
**Branch**: `006-firmware-update` | **Date**: 2026-07-15 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/006-firmware-update/spec.md`
**Note**: This plan was reverse-engineered from the existing `feature/firmware` module as part of a brownfield migration.
## Summary
The Firmware Update feature provides a complete firmware flashing pipeline for Meshtastic devices across three transport mechanisms (BLE, WiFi/TCP, USB) and three device architectures (ESP32, nRF52, RP2040). The implementation uses a handler-router pattern (`FirmwareUpdateManager` → transport-specific handlers) with platform-abstracted file I/O (`FirmwareFileHandler`) and a state-machine-driven Compose Multiplatform UI. All protocol implementations (ESP32 Unified OTA, Nordic Secure DFU, Nordic Legacy DFU) are pure Kotlin in `commonMain`.
## Technical Context
**Language/Version**: Kotlin 2.3+ targeting JDK 21
**Primary Dependencies**: Compose Multiplatform, Material 3 Adaptive + Expressive, Koin 4.2+ (K2 Compiler Plugin), Ktor (raw sockets for WiFi OTA), Okio (SHA-256 hashing), Kable (BLE via `core/ble`), Coil 3 (device images), mikepenz/multiplatform-markdown-renderer (release notes)
**Storage**: `BootloaderWarningDataSource` (DataStore KMP) for bootloader dismissal persistence
**Testing**: KMP `allTests` — 21 test files in `commonTest`, 1 in `jvmTest`. Uses Mokkery for mocks, `FakeNodeRepository` / `FakeRadioController` for fakes.
**Target Platform**: Android, Desktop (JVM) — all via `commonMain`
**Performance Goals**: Real-time throughput tracking via sliding-window `ThroughputTracker`; BLE DFU throughput ~1-12 KiB/s (20-244 byte packets); WiFi OTA ~50-100 KiB/s (1024-byte chunks)
**Constraints**: All UI in `commonMain`; no `java.*`/`android.*` in common; CMP float pre-formatting via `NumberFormatter.format()`; `safeCatching {}` not `runCatching {}`
**Scale/Scope**: 30 source files, 2 platform files (androidMain), 2 platform files (jvmMain), 22 test files
## Constitution Check
*GATE: All principles verified against existing implementation.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Kotlin Multiplatform Core | ✅ PASS | All 30 source files in `commonMain`. Platform code limited to `FirmwareFileHandler` impls and `FirmwareUsbManager` impls. |
| II. Zero Lint Tolerance | ✅ PASS | `spotlessApply` + `detekt` pass. `@Suppress` annotations documented where needed (e.g., `LongParameterList`, `MagicNumber`). |
| III. Compose Multiplatform UI | ✅ PASS | CMP composables throughout. `NumberFormatter.format()` used for throughput display. Navigation 3 patterns via `firmwareGraph()`. |
| IV. Privacy First | ✅ PASS | No PII logged. Firmware hashes are ephemeral. Network calls only to public GitHub URLs. |
| V. Design Standards Compliance | ✅ PASS | M3 Expressive APIs (`CircularWavyProgressIndicator`, `LinearWavyProgressIndicator`, `ButtonDefaults.LargeContainerHeight`). `MeshtasticIcons` exclusively. |
| VI. Verify Before Push | ✅ PASS | Full verification pipeline passes: `spotlessApply spotlessCheck detekt assembleDebug test allTests`. |
| VII. Coroutine Safety | ✅ PASS | `safeCatching {}` used consistently. `ioDispatcher` from project utils. `NonCancellable` for cleanup in `onCleared()`. |
| VIII. Resource Discipline | ✅ PASS | All strings via `stringResource(Res.string.*)`. Icons from `MeshtasticIcons`. |
| IX. Branch & Scope Hygiene | ✅ PASS | Feature module is self-contained with clean dependency boundaries. |
**Gate Result**: ✅ All principles satisfied
## Project Structure
### Documentation (this feature)
```text
specs/006-firmware-update/
├── spec.md # Feature specification (migrated)
├── plan.md # This file (migrated)
└── tasks.md # Task breakdown (migrated)
```
### Source Code (repository root)
```text
feature/firmware/
├── src/commonMain/kotlin/org/meshtastic/feature/firmware/
│ ├── DefaultFirmwareUpdateManager.kt ← Handler router (BLE/WiFi/USB × ESP32/nRF52)
│ ├── FirmwareArtifact.kt ← Platform-neutral firmware file handle
│ ├── FirmwareFileHandler.kt ← Platform I/O interface + isValidFirmwareFile()
│ ├── FirmwareManifest.kt ← .mt.json manifest model (kotlinx.serialization)
│ ├── FirmwareRetriever.kt ← Multi-strategy firmware download (manifest → heuristics → zip)
│ ├── FirmwareUpdateActions.kt ← UI callback holder (lambda bundle)
│ ├── FirmwareUpdateHandler.kt ← Common handler interface
│ ├── FirmwareUpdateManager.kt ← Manager interface
│ ├── FirmwareUpdateScreen.kt ← CMP UI (889 lines — scaffold, progress, error, success)
│ ├── FirmwareUpdateState.kt ← Sealed state machine (Idle→Checking→Ready→...→Success)
│ ├── FirmwareUpdateViewModel.kt ← ViewModel (update orchestration, verification, cleanup)
│ ├── FirmwareUsbManager.kt ← USB detach flow interface
│ ├── UsbUpdateHandler.kt ← USB/UF2 handler
│ ├── UsbUpdateSupport.kt ← Shared USB update logic (top-level function)
│ ├── di/
│ │ └── FeatureFirmwareModule.kt ← Koin DI module (@ComponentScan)
│ ├── navigation/
│ │ └── FirmwareNavigation.kt ← Navigation 3 entry provider
│ └── ota/
│ ├── BleOtaTransport.kt ← BLE transport for ESP32 Unified OTA
│ ├── BleScanSupport.kt ← BLE scan helpers + MAC+1 calculation
│ ├── Esp32OtaUpdateHandler.kt ← ESP32 OTA orchestrator (BLE + WiFi)
│ ├── FirmwareHashUtil.kt ← SHA-256 via Okio
│ ├── ThroughputTracker.kt ← Sliding-window speed calculator
│ ├── UnifiedOtaProtocol.kt ← OTA command/response/exception models
│ ├── WifiOtaTransport.kt ← WiFi/TCP transport via Ktor raw sockets
│ └── dfu/
│ ├── DfuUploadTransport.kt ← Common DFU upload interface
│ ├── DfuZipParser.kt ← Nordic DFU zip → DfuZipPackage
│ ├── LegacyDfuProtocol.kt ← Legacy DFU opcodes, responses, payloads
│ ├── LegacyDfuTransport.kt ← Legacy DFU BLE transport (Adafruit BLEDfu)
│ ├── SecureDfuHandler.kt ← nRF52 DFU orchestrator (Secure + Legacy auto-detect)
│ ├── SecureDfuProtocol.kt ← Secure DFU opcodes, responses, CRC-32, manifest
│ └── SecureDfuTransport.kt ← Secure DFU BLE transport (FE59)
├── src/androidMain/kotlin/org/meshtastic/feature/firmware/
│ ├── AndroidFirmwareFileHandler.kt ← Android file I/O (ContentResolver, OkHttp)
│ └── AndroidFirmwareUsbManager.kt ← Android USB device detach flow
├── src/jvmMain/kotlin/org/meshtastic/feature/firmware/
│ ├── JvmFirmwareFileHandler.kt ← Desktop file I/O (java.io)
│ └── DesktopFirmwareUsbManager.kt ← Desktop USB stub
├── src/commonTest/kotlin/org/meshtastic/feature/firmware/
│ ├── CommonFirmwareRetrieverTest.kt ← ESP32 manifest/heuristic resolution (abstract)
│ ├── CommonPerformUsbUpdateTest.kt ← USB update flow tests (abstract)
│ ├── DefaultFirmwareUpdateManagerTest.kt ← Handler routing tests
│ ├── FirmwareManifestTest.kt ← Manifest deserialization
│ ├── FirmwareUpdateIntegrationTest.kt ← End-to-end ViewModel integration
│ ├── FirmwareUpdateStateTest.kt ← ProgressState + stripFormatArgs
│ ├── FirmwareUpdateViewModelTest.kt ← ViewModel unit tests
│ ├── IsValidFirmwareFileTest.kt ← Firmware filename validation
│ ├── TestApplicationCoroutineScope.kt ← Test helper
│ └── ota/
│ ├── BleOtaTransportTest.kt ← BLE OTA transport tests
│ ├── BleScanSupportTest.kt ← MAC+1 calculation tests
│ ├── FirmwareHashUtilTest.kt ← SHA-256 tests
│ ├── OtaResponseTest.kt ← OTA response parsing
│ ├── ThroughputTrackerTest.kt ← Throughput calculation tests
│ └── dfu/
│ ├── DfuCrc32Test.kt ← CRC-32 tests
│ ├── DfuResponseTest.kt ← DFU response parsing
│ ├── DfuZipParserTest.kt ← DFU zip parsing
│ ├── LegacyDfuProtocolTest.kt ← Legacy DFU protocol tests
│ ├── LegacyDfuTransportTest.kt ← Legacy DFU transport tests
│ ├── SecureDfuProtocolTest.kt ← Secure DFU protocol tests
│ └── SecureDfuTransportTest.kt ← Secure DFU transport tests
└── src/jvmTest/kotlin/org/meshtastic/feature/firmware/
└── FirmwareUpdateViewModelFileTest.kt ← JVM-specific file operation tests
```
**Structure Decision**: The feature follows the established `feature/*` module pattern. The `ota/` and `ota/dfu/` sub-packages organize protocol implementations by transport family. All protocol models, commands, and responses are co-located with their transport implementations for cohesion.
## Module Impact
| Module | Change Type | Files Affected | Risk |
|--------|-------------|----------------|------|
| `feature/firmware` | All New | 35 source + 22 test | Low (isolated module) |
| `core/ble` | Dependency | 0 (uses existing interfaces) | Low |
| `core/model` | Dependency | 0 (uses `DeviceHardware`, `RadioController`) | Low |
| `core/database` | Dependency | 0 (uses `FirmwareRelease` entity) | Low |
| `core/resources` | Modify | 1 file (strings.xml — ~50 firmware_update_* strings) | Low |
## Integration Points
- **Navigation**: `FirmwareNavigation.firmwareGraph()` registers `FirmwareRoute.FirmwareGraph` and `FirmwareRoute.FirmwareUpdate` entries into Navigation 3.
- **DI**: `FeatureFirmwareModule` uses `@ComponentScan` to auto-register all `@Single` and `@KoinViewModel` annotated classes.
- **Radio Controller**: Uses `RadioController.setDeviceAddress("n")` to disconnect mesh service before OTA, `rebootToDfu()` / `requestRebootOta()` to trigger device reboot.
- **DataStore**: `BootloaderWarningDataSource` persists per-device dismissal of bootloader upgrade warnings.
- **BLE**: Depends on `core/ble` abstractions (`BleScanner`, `BleConnectionFactory`, `BleConnection`) for all BLE operations.
## Design Constraints
- All UI lives in `commonMain` — not platform-specific
- Strings accessed via `stringResource(Res.string.key)` — never hardcoded
- Icons use `MeshtasticIcons` exclusively (from `core/ui/icon/`)
- Error handling uses `safeCatching {}` not `runCatching {}`
- Dispatchers via `org.meshtastic.core.common.util.ioDispatcher`
- Float values must be pre-formatted with `NumberFormatter.format()` (CMP constraint)
- Legacy DFU packet size defaults to 20 bytes for safety; OTAFIX-2.1+ devices use negotiated MTU up to 244 bytes
- ESP32 OTA requires mesh service disconnect before transport connection (GATT exclusivity)
## Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| BLE link drops mid-DFU transfer | Medium | High | Connection-state watchers cancel streaming immediately; abort sent to device; retry logic in SecureDfuTransport |
| Bootloader protocol mismatch (Secure vs Legacy) | Low | High | Auto-detection via BLE scan for Legacy service UUID before connecting |
| Firmware hash rejected by ESP32 | Low | Medium | Specific `HashRejected` exception with user-facing error message |
| UF2 file save fails on Android | Low | Medium | Save dialog with retry; `AwaitingFileSave` state persists until user acts |
| OTAFIX-2.1 high-MTU packets overrun non-OTAFIX bootloaders | Low | Critical | Name-suffix detection (`_DFU`) gates high-MTU; defaults to safe 20-byte packets |
## Phase Alignment with Tasks
| Phase | Purpose | Key Tasks | Dependencies |
|-------|---------|-----------|--------------|
| 1. Core Models & Interfaces | Data types, state machine, handler interface | FW-T001FW-T006 | None |
| 2. Firmware Retrieval | Download, manifest resolution, zip extraction | FW-T007FW-T012 | Phase 1 |
| 3. OTA Protocols | ESP32 OTA (BLE + WiFi), Nordic DFU (Secure + Legacy), USB | FW-T013FW-T028 | Phase 2 |
| 4. ViewModel & UI | Screen composables, ViewModel orchestration, navigation | FW-T029FW-T037 | Phase 3 |
| 5. Testing & Polish | Unit tests, integration tests, DI module | FW-T038FW-T042 | All prior phases |
### Critical Path
```
Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5
```
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| *None* | — | — |

View File

@@ -1,241 +0,0 @@
# Feature Specification: Firmware Update (OTA / DFU)
**Feature Branch**: `006-firmware-update`
**Created**: 2026-07-15
**Status**: Migrated
**Input**: Brownfield migration — reverse-engineered from existing `feature/firmware` module
## Summary
Firmware Update provides end-to-end over-the-air (OTA) and USB firmware flashing for all Meshtastic device families. The feature auto-detects the connected device's hardware model and active transport (BLE, WiFi/TCP, USB/Serial), downloads the correct firmware binary from the Meshtastic release infrastructure, and executes the appropriate update protocol: ESP32 Unified OTA (BLE or WiFi), Nordic Secure DFU (nRF52), Nordic Legacy DFU / Adafruit BLEDfu (nRF52), or UF2 USB Mass Storage (nRF52/RP2040). All business logic, protocol implementations, and Compose Multiplatform UI reside in `commonMain`; only file I/O and USB manager adapters live in platform source sets.
## Goals
1. Allow users to flash stable, alpha, or local firmware files to any connected Meshtastic device with a single tap.
2. Support all three transport mechanisms — BLE DFU, WiFi/TCP OTA, and USB UF2 — with automatic routing based on connection type and device architecture.
3. Display real-time download and upload progress with throughput metrics (KiB/s, ETA).
4. Verify the device reconnects after flashing and report success or verification failure.
5. Provide safety guardrails: battery level checks, bootloader upgrade warnings, disclaimer dialogs, and screen-on locks during transfer.
## Non-Goals
- Bootloader upgrade itself — the feature warns about outdated bootloaders and links to documentation, but does not perform the upgrade.
- ESP32 firmware update over USB/Serial — explicitly unsupported; shown as `Unknown` update method.
- Firmware building or custom build pipelines — the feature only consumes published release artifacts.
- Multi-device batch updates — only the currently connected device can be updated.
- iOS platform support — `FirmwareFileHandler` and `FirmwareUsbManager` have no `iosMain` implementations yet.
## User Scenarios & Testing *(mandatory)*
### User Story 1 — Flash Stable Firmware via BLE (Priority: P1)
A user with a BLE-connected nRF52 device navigates to the Firmware Update screen, sees the currently installed version and the latest stable release, taps "Update via BLE", acknowledges the disclaimer, and watches the firmware download and flash to the device. After the device reboots, the app verifies reconnection and shows a success screen.
**Why this priority**: BLE + nRF52 is the most common device/transport combination in the field. This is the primary update path for RAK4631, T114, and similar boards.
**Independent Test**: Can be fully tested by connecting to any nRF52 device over BLE. Delivers the core value of keeping devices on the latest firmware.
**Acceptance Scenarios**:
1. **Given** a BLE-connected nRF52 device on firmware 2.4.0, **When** the user opens the Firmware Update screen, **Then** the screen shows the device name, current version (2.4.0), latest stable release, and an "Update via BLE" button.
2. **Given** the user taps "Update via BLE" and confirms the disclaimer, **When** the download completes and DFU begins, **Then** a progress bar shows upload percentage, speed (KiB/s), and ETA.
3. **Given** the DFU transfer completes successfully, **When** the device reboots, **Then** the app enters "Verifying" state, reconnects within 60 seconds, and shows "Success".
4. **Given** the device does not reconnect within the 60-second timeout, **Then** the app shows "Verification Failed" with Retry and Done options.
---
### User Story 2 — Flash ESP32 Firmware via BLE OTA (Priority: P1)
A user with a BLE-connected ESP32 device (e.g., Heltec V3, T-Deck) updates firmware using the ESP32 Unified OTA protocol over BLE. The app downloads the correct `.bin` file (resolved via `.mt.json` manifest or filename heuristics), triggers an OTA reboot, reconnects to the device in OTA mode at MAC+1, streams the firmware with SHA-256 verification, and confirms success.
**Why this priority**: ESP32 is the second major architecture family, and BLE is the primary transport for mobile users.
**Independent Test**: Connect to any ESP32-based device over BLE to test the full OTA flow.
**Acceptance Scenarios**:
1. **Given** a BLE-connected ESP32-S3 device, **When** the user initiates an update, **Then** the app resolves the firmware binary via the `.mt.json` manifest (or falls back through naming heuristics).
2. **Given** the firmware is downloaded, **When** the OTA reboot is triggered, **Then** the app disconnects the mesh service, scans for the device at the original or MAC+1 address, and connects to the OTA service.
3. **Given** a successful OTA connection, **When** the firmware is streamed, **Then** the device validates the SHA-256 hash and responds with "OK".
4. **Given** the device rejects the hash, **Then** the app shows a "Hash Rejected" error with guidance.
---
### User Story 3 — Flash ESP32 Firmware via WiFi/TCP OTA (Priority: P2)
A user with a TCP-connected ESP32 device updates firmware using the ESP32 Unified OTA protocol over a raw TCP socket. The flow is identical to BLE OTA but uses Ktor raw sockets on port 3232 with larger chunk sizes (1024 bytes vs 512 for BLE).
**Why this priority**: WiFi OTA is faster than BLE and preferred by power users with network-connected devices, but is less common than BLE.
**Independent Test**: Connect to any ESP32 device via TCP/WiFi to test the WiFi OTA flow.
**Acceptance Scenarios**:
1. **Given** a TCP-connected ESP32 device at `192.168.1.100`, **When** the user initiates a firmware update, **Then** the app connects to the device on port 3232 via Ktor raw sockets.
2. **Given** a successful TCP connection, **When** the firmware is streamed, **Then** the transfer uses 1024-byte chunks and completes without per-packet ACK overhead.
3. **Given** the device verifies the hash after transfer, **When** "OK" is received, **Then** the app transitions to Success state.
---
### User Story 4 — Flash nRF52/RP2040 Firmware via USB (Priority: P2)
A user with a USB/Serial-connected nRF52 or RP2040 device updates firmware by downloading the `.uf2` file, rebooting the device into DFU bootloader mode, and saving the UF2 to the device's virtual mass storage. The app handles the download, triggers `rebootToDfu`, and prompts the user to save the file.
**Why this priority**: USB is the only update path for devices without BLE (e.g., desktop-connected RP2040 boards).
**Independent Test**: Connect any nRF52/RP2040 device via USB serial to test the UF2 save flow.
**Acceptance Scenarios**:
1. **Given** a serial-connected nRF52 device, **When** the user initiates a firmware update, **Then** the app downloads the `.uf2` file and shows a "Rebooting" state.
2. **Given** the device reboots into DFU mode, **When** the UF2 file is ready, **Then** the app presents an `AwaitingFileSave` dialog with instructions.
3. **Given** the user saves the UF2 file to the device, **When** the device detaches and reboots, **Then** the app verifies reconnection.
---
### User Story 5 — Flash Local Firmware File (Priority: P2)
A user selects "Local File" as the release type, picks a firmware file (`.zip` for BLE DFU, `.bin` for ESP32 OTA, `.uf2` for USB) from device storage, and the app applies it using the appropriate handler. This supports beta testers and developers with custom builds.
**Why this priority**: Essential for development and testing, but not used by mainstream users.
**Independent Test**: Pick a local firmware file and apply it to any connected device.
**Acceptance Scenarios**:
1. **Given** the user selects the "Local File" tab, **When** they tap "Select File", **Then** a file picker opens accepting `*/*`.
2. **Given** a local `.zip` file is selected for a BLE nRF52 device, **When** the file is processed, **Then** the app extracts the DFU package and begins the transfer.
3. **Given** a local `.bin` file is selected for a BLE ESP32 device with a valid Bluetooth address, **When** the file is processed, **Then** the app imports it and starts the OTA update with a synthetic `LOCAL` release.
4. **Given** a BLE ESP32 update from file but the Bluetooth address is invalid, **Then** the app shows a "No device" error.
---
### User Story 6 — Bootloader Warning & Safety Guards (Priority: P3)
Users with devices that require a bootloader upgrade before OTA (flagged via `requiresBootloaderUpgradeForOta`) see a prominent warning card with a "Learn More" link and a "Don't show again" dismissal option. Additionally, firmware updates are blocked if battery level is ≤10%, the screen stays on during transfers, and a back-navigation confirmation dialog prevents accidental cancellation.
**Why this priority**: Safety features that prevent bricked devices and failed updates — important but secondary to the core update flow.
**Independent Test**: Connect a flagged nRF52 device (e.g., RAK4631) over BLE to verify the warning card appears and can be dismissed.
**Acceptance Scenarios**:
1. **Given** a BLE-connected device with `requiresBootloaderUpgradeForOta = true`, **When** the user opens the Firmware Update screen, **Then** a red warning card is displayed with the device name and a "Learn More" link.
2. **Given** the user taps "Don't show again", **Then** the warning is dismissed for that device address and does not reappear.
3. **Given** a device with battery level at 5%, **When** the user taps "Update", **Then** the app shows a "Battery low" error and does not start the update.
4. **Given** a firmware transfer is in progress (Downloading/Processing/Updating/Verifying), **When** the user presses back, **Then** a confirmation dialog appears instead of navigating away.
---
### Edge Cases
- What happens when the device disconnects mid-transfer? → BLE transports detect link drops via connection state watchers and surface `ConnectionFailed` / `TransferFailed` errors.
- What happens when the firmware hash is rejected by the device? → The `HashRejected` OTA exception is caught and a specific "Hash Rejected" error message is shown.
- What happens when the DFU zip is malformed? → `parseDfuZipEntries` throws `DfuException.InvalidPackage` with a descriptive message (missing manifest, missing bin/dat).
- What happens when no matching firmware file is found in the release? → The retriever returns `null`, and the handler shows a "Firmware not found for [device]" error.
- What happens when the Legacy DFU bootloader is too old (SDK ≤ 6)? → `LegacyDfuException.UnsupportedBootloader` is thrown with guidance to update the bootloader.
- What happens when BLE packets are lost during Secure DFU? → The transport detects bytes-lost via CRC checksum, tightens PRN to 1, and resends the lost portion.
## Architecture
### Key Components
| Component | Module / File | Purpose |
|-----------|---------------|---------|
| `FirmwareUpdateScreen` | `feature/firmware/FirmwareUpdateScreen.kt` | CMP UI — state-driven scaffold with progress, error, success, and file-save composables |
| `FirmwareUpdateViewModel` | `feature/firmware/FirmwareUpdateViewModel.kt` | Coordinates release checking, update execution, post-update verification, and temp file cleanup |
| `FirmwareUpdateManager` | `feature/firmware/FirmwareUpdateManager.kt` | Interface routing update requests to the correct handler based on connection type + architecture |
| `DefaultFirmwareUpdateManager` | `feature/firmware/DefaultFirmwareUpdateManager.kt` | Routes to `SecureDfuHandler`, `Esp32OtaUpdateHandler`, or `UsbUpdateHandler` |
| `FirmwareRetriever` | `feature/firmware/FirmwareRetriever.kt` | Downloads firmware via manifest resolution, filename heuristics, or zip extraction fallback |
| `FirmwareFileHandler` | `feature/firmware/FirmwareFileHandler.kt` | Platform-abstracted file/network I/O interface (androidMain / jvmMain implementations) |
| `Esp32OtaUpdateHandler` | `feature/firmware/ota/Esp32OtaUpdateHandler.kt` | ESP32 OTA orchestrator — triggers reboot, connects transport, streams firmware |
| `BleOtaTransport` | `feature/firmware/ota/BleOtaTransport.kt` | BLE transport for ESP32 Unified OTA protocol using Kable |
| `WifiOtaTransport` | `feature/firmware/ota/WifiOtaTransport.kt` | WiFi/TCP transport for ESP32 Unified OTA using Ktor raw sockets |
| `SecureDfuHandler` | `feature/firmware/ota/dfu/SecureDfuHandler.kt` | nRF52 DFU orchestrator — auto-detects Secure vs Legacy bootloader protocol |
| `SecureDfuTransport` | `feature/firmware/ota/dfu/SecureDfuTransport.kt` | Nordic Secure DFU (FE59) BLE transport with object-transfer, CRC-32, and PRN flow control |
| `LegacyDfuTransport` | `feature/firmware/ota/dfu/LegacyDfuTransport.kt` | Nordic Legacy DFU (1530) / Adafruit BLEDfu transport with PRN and OTAFIX-2.1 high-MTU support |
| `UsbUpdateHandler` | `feature/firmware/UsbUpdateHandler.kt` | USB/UF2 update handler — downloads UF2, reboots to DFU, presents save dialog |
| `ThroughputTracker` | `feature/firmware/ota/ThroughputTracker.kt` | Sliding-window throughput calculator for real-time speed/ETA display |
| `FirmwareHashUtil` | `feature/firmware/ota/FirmwareHashUtil.kt` | SHA-256 hashing via Okio for firmware integrity verification |
| `DfuZipParser` | `feature/firmware/ota/dfu/DfuZipParser.kt` | Parses Nordic DFU zip packages (manifest.json → .dat + .bin) |
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST auto-detect the connected device's hardware model via `DeviceHardwareRepository` and resolve the correct firmware binary.
- **FR-002**: System MUST support three release channels: Stable, Alpha, and Local File, selectable via a segmented button row.
- **FR-003**: System MUST route firmware updates to the correct handler based on connection type (BLE → DFU/OTA, TCP → WiFi OTA, Serial → USB UF2) and architecture (ESP32 → OTA, nRF52 → DFU/USB).
- **FR-004**: System MUST download firmware from the Meshtastic GitHub release infrastructure, resolving via `.mt.json` manifest first, then filename heuristics, then zip extraction as fallback.
- **FR-005**: System MUST compute SHA-256 of ESP32 firmware and include it in the OTA handshake for device-side verification.
- **FR-006**: System MUST compute running CRC-32 checksums during Secure DFU transfers and validate against device-reported values at PRN intervals.
- **FR-007**: System MUST support both Nordic Secure DFU (service `FE59`) and Nordic Legacy DFU / Adafruit BLEDfu (service `1530`) protocols, auto-detecting which the bootloader speaks.
- **FR-008**: System MUST support buttonless DFU trigger for both Secure and Legacy services, with fallback from Secure to Legacy when FE59 is not exposed.
- **FR-009**: System MUST display download and upload progress with percentage, throughput (KiB/s), and ETA.
- **FR-010**: System MUST verify the device reconnects after flashing within a 60-second timeout.
- **FR-011**: System MUST block firmware updates when battery level is ≤ 10%.
- **FR-012**: System MUST show a bootloader upgrade warning card for devices with `requiresBootloaderUpgradeForOta = true` over BLE, dismissable per device address.
- **FR-013**: System MUST show a disclaimer dialog before starting any update, with a disconnect warning and "I know what I'm doing" confirmation.
- **FR-014**: System MUST keep the screen on during active transfer states (Downloading, Processing, Updating, Verifying).
- **FR-015**: System MUST clean up temporary firmware files on ViewModel destruction (via `ApplicationCoroutineScope` + `NonCancellable`).
- **FR-016**: System MUST support resume for Secure DFU — if the device already has partial data with a matching CRC, skip to the next object boundary.
- **FR-017**: System MUST handle OTAFIX-2.1+ bootloaders by detecting the `_DFU` advertising name suffix and using high-MTU packets (up to 244 bytes) for Legacy DFU.
- **FR-018**: System MUST handle bytes-lost during Secure DFU by tightening PRN to 1 and resending the lost portion.
### Non-Functional Requirements
- **NFR-001**: All protocol implementations and UI composables MUST reside in `commonMain` — no `android.*` or `java.*` imports.
- **NFR-002**: BLE OTA chunk size MUST be 512 bytes; WiFi OTA chunk size MUST be 1024 bytes for optimal throughput.
- **NFR-003**: Connection timeouts MUST be ≤ 15 seconds; command timeouts ≤ 30 seconds; erasing timeouts ≤ 60 seconds.
- **NFR-004**: The feature MUST use `safeCatching {}` (not `runCatching {}`) and project `ioDispatcher` (not `Dispatchers.IO`) per constitution.
## Source-Set Impact
| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | 30 source files (~5,697 lines) | All business logic, protocols, and UI |
| `androidMain` | 2 files (`AndroidFirmwareFileHandler`, `AndroidFirmwareUsbManager`) | Platform file I/O and USB device detection |
| `jvmMain` | 2 files (`JvmFirmwareFileHandler`, `DesktopFirmwareUsbManager`) | Desktop file I/O and stub USB manager |
| `commonTest` | 21 test files (~4,602 lines) | Protocol, retriever, ViewModel, state, and integration tests |
| `jvmTest` | 1 file (`FirmwareUpdateViewModelFileTest`) | JVM-specific ViewModel file operation tests |
## Design Standards Compliance
- [x] New screens reviewed against [design standards](https://raw.githubusercontent.com/meshtastic/design/refs/heads/master/standards/meshtastic_design_standards_latest.md)
- [x] M3 component selection verified — `SegmentedButton`, `ElevatedCard`, `LinearWavyProgressIndicator`, `CircularWavyProgressIndicator`, `MeshtasticDialog`
- [x] Accessibility: haptic feedback on update/success actions, descriptive content descriptions on icons
- [x] Typography: `headlineSmall` for device name, `titleMedium` for status messages, `bodyMedium`/`bodySmall` for details
## Privacy Assessment
- [x] No PII, location data, or cryptographic keys logged or exposed — firmware hashes are logged but are not user data
- [x] No new network calls that transmit user data — only firmware downloads from public GitHub release URLs
- [x] Proto submodule (`core/proto`) not modified (read-only upstream)
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Firmware updates succeed end-to-end for all supported architectures (ESP32, nRF52, RP2040) across all transport types (BLE, WiFi, USB).
- **SC-002**: ESP32 firmware resolution correctly uses `.mt.json` manifest when available and falls back gracefully through 3 additional strategies.
- **SC-003**: Nordic DFU handler auto-detects Secure vs Legacy protocol and routes to the correct transport.
- **SC-004**: Post-update device verification detects reconnection within 60 seconds or reports verification failure.
- **SC-005**: Battery check prevents updates at ≤ 10% charge.
- **SC-006**: Bootloader warning is shown for flagged devices over BLE and persists dismissal per address.
- **SC-007**: Temporary firmware files are cleaned up on ViewModel destruction and on init.
- **SC-008**: All 21 test files (4,602 lines) pass in `allTests`.
- **SC-009**: Upload progress displays accurate throughput (KiB/s) and ETA using sliding-window tracker.
- **SC-010**: Screen stays on during active transfer states and back-navigation shows confirmation dialog.
## Assumptions
- All business logic and UI composables reside in `commonMain` source set
- String resources added to `core/resources/src/commonMain/composeResources/values/strings.xml`
- Icons use `MeshtasticIcons` (from `core/ui/icon/`)
- Float values pre-formatted with `NumberFormatter.format()` (CMP constraint)
- Device hardware metadata (architecture, platformioTarget, bootloaderInfoUrl) is available from `DeviceHardwareRepository`
- BLE abstraction layer (`core/ble`) provides `BleScanner`, `BleConnectionFactory`, and `BleConnection` interfaces
- The Meshtastic firmware release infrastructure serves files at `https://raw.githubusercontent.com/meshtastic/meshtastic.github.io/master/firmware-{version}/`
- DFU zip packages follow the Nordic DFU format: `manifest.json` + `.dat` + `.bin`
- ESP32 devices advertise at MAC+1 in OTA mode; nRF52 devices advertise at MAC+1 in DFU mode

View File

@@ -1,284 +0,0 @@
# Tasks: Firmware Update (OTA / DFU)
**Spec**: [spec.md](spec.md) | **Plan**: [plan.md](plan.md) | **Status**: Migrated
**Task Prefix**: `FW-T`
> All tasks marked `[x]` were reverse-engineered from the existing implementation.
> Tasks marked `[ ]` are identified gaps — work that should be done to improve the feature.
---
## Phase 1 — Core Models & Interfaces
- [x] **FW-T001**: Define `FirmwareArtifact` data class
`feature/firmware/src/commonMain/.../FirmwareArtifact.kt`
Platform-neutral handle for firmware files with `uri`, `fileName`, and `isTemporary` properties.
- [x] **FW-T002**: Define `FirmwareUpdateState` sealed interface
`feature/firmware/src/commonMain/.../FirmwareUpdateState.kt`
State machine: `Idle`, `Checking`, `Ready`, `Downloading`, `Processing`, `Updating`, `Verifying`, `VerificationFailed`, `Error`, `Success`, `AwaitingFileSave`.
- [x] **FW-T003**: Define `ProgressState` data class
`feature/firmware/src/commonMain/.../FirmwareUpdateState.kt`
Progress container with `message: UiText`, `progress: Float`, `details: String?`.
- [x] **FW-T004**: Define `FirmwareUpdateMethod` sealed class
`feature/firmware/src/commonMain/.../FirmwareUpdateViewModel.kt`
Transport mechanism enum: `Usb`, `Ble`, `Wifi`, `Unknown` — each with a `StringResource` description.
- [x] **FW-T005**: Define `FirmwareUpdateHandler` interface
`feature/firmware/src/commonMain/.../FirmwareUpdateHandler.kt`
Common `startUpdate()` contract for all handlers (release, hardware, target, state callback, optional URI).
- [x] **FW-T006**: Define `FirmwareUpdateManager` interface
`feature/firmware/src/commonMain/.../FirmwareUpdateManager.kt`
Routes update requests to the appropriate handler. `startUpdate()` returns a `FirmwareArtifact?` for cleanup.
- [x] **FW-T007**: Define `FirmwareUpdateActions` data class
`feature/firmware/src/commonMain/.../FirmwareUpdateActions.kt`
Lambda bundle for UI callbacks: `onReleaseTypeSelect`, `onStartUpdate`, `onPickFile`, `onSaveFile`, `onRetry`, `onCancel`, `onDone`, `onDismissBootloaderWarning`.
---
## Phase 2 — Firmware Retrieval
- [x] **FW-T008**: Define `FirmwareFileHandler` interface
`feature/firmware/src/commonMain/.../FirmwareFileHandler.kt`
Platform-abstracted file/network I/O: download, extract, copy, delete, zip operations.
- [x] **FW-T009**: Implement `isValidFirmwareFile()` utility
`feature/firmware/src/commonMain/.../FirmwareFileHandler.kt`
Filters firmware binaries from non-firmware artifacts (`littlefs-*`, `bleota*`, `mt-*`, `*.factory.*`).
- [x] **FW-T010**: Define `FirmwareManifest` and `FirmwareManifestFile` models
`feature/firmware/src/commonMain/.../FirmwareManifest.kt`
Kotlin model for `.mt.json` manifest files (kotlinx.serialization). Locates the `app0` OTA partition entry.
- [x] **FW-T011**: Implement `FirmwareRetriever`
`feature/firmware/src/commonMain/.../FirmwareRetriever.kt`
Multi-strategy firmware download: manifest → current naming → legacy naming → zip extraction. Supports `retrieveOtaFirmware()` (nRF52 DFU), `retrieveUsbFirmware()` (UF2), and `retrieveEsp32Firmware()` (ESP32 OTA).
- [x] **FW-T012**: Implement `AndroidFirmwareFileHandler`
`feature/firmware/src/androidMain/.../AndroidFirmwareFileHandler.kt`
Android-specific file I/O using ContentResolver and OkHttp.
- [x] **FW-T013**: Implement `JvmFirmwareFileHandler`
`feature/firmware/src/jvmMain/.../JvmFirmwareFileHandler.kt`
Desktop-specific file I/O using `java.io`.
---
## Phase 3 — OTA & DFU Protocols
### ESP32 Unified OTA
- [x] **FW-T014**: Define `UnifiedOtaProtocol` interface
`feature/firmware/src/commonMain/.../ota/UnifiedOtaProtocol.kt`
`connect()`, `startOta()`, `streamFirmware()`, `close()`. Shared by BLE and WiFi transports.
- [x] **FW-T015**: Define OTA commands, responses, and exceptions
`feature/firmware/src/commonMain/.../ota/UnifiedOtaProtocol.kt`
`OtaCommand.StartOta`, `OtaResponse` (Ok, Erasing, Ack, Error), `OtaProtocolException` hierarchy.
- [x] **FW-T016**: Implement `BleOtaTransport`
`feature/firmware/src/commonMain/.../ota/BleOtaTransport.kt`
BLE transport using Kable. Scans for OTA service UUID, connects, subscribes to notify characteristic, writes firmware in 512-byte chunks.
- [x] **FW-T017**: Implement `WifiOtaTransport`
`feature/firmware/src/commonMain/.../ota/WifiOtaTransport.kt`
WiFi/TCP transport using Ktor raw sockets on port 3232. 1024-byte chunks, no per-chunk ACK.
- [x] **FW-T018**: Implement `Esp32OtaUpdateHandler`
`feature/firmware/src/commonMain/.../ota/Esp32OtaUpdateHandler.kt`
Orchestrator: obtain firmware → compute SHA-256 → trigger OTA reboot → disconnect mesh → connect transport → stream → report success.
- [x] **FW-T019**: Implement `FirmwareHashUtil` (SHA-256 via Okio)
`feature/firmware/src/commonMain/.../ota/FirmwareHashUtil.kt`
`calculateSha256Bytes()` and `bytesToHex()` using `ByteString.sha256()`.
- [x] **FW-T020**: Implement `ThroughputTracker`
`feature/firmware/src/commonMain/.../ota/ThroughputTracker.kt`
Sliding-window throughput calculator with configurable window size and `TimeSource`.
- [x] **FW-T021**: Implement BLE scan support utilities
`feature/firmware/src/commonMain/.../ota/BleScanSupport.kt`
`calculateMacPlusOne()` for OTA/DFU address, `scanForBleDevice()` with retry logic.
### Nordic DFU (nRF52)
- [x] **FW-T022**: Define Secure DFU protocol models
`feature/firmware/src/commonMain/.../ota/dfu/SecureDfuProtocol.kt`
UUIDs, opcodes, result codes, extended errors, `DfuResponse` parsing, `DfuCrc32`, `DfuZipPackage`, `DfuManifest`, `DfuException` hierarchy.
- [x] **FW-T023**: Implement `SecureDfuTransport`
`feature/firmware/src/commonMain/.../ota/dfu/SecureDfuTransport.kt`
Full Nordic Secure DFU (FE59): buttonless trigger (Secure + Legacy fallback), DFU-mode connect, init packet transfer, firmware streaming with PRN flow control and CRC-32 validation, object resume, bytes-lost recovery.
- [x] **FW-T024**: Define Legacy DFU protocol models
`feature/firmware/src/commonMain/.../ota/dfu/LegacyDfuProtocol.kt`
Characteristic UUIDs, opcodes, status codes, `LegacyDfuResponse` parsing, payload builders, `LegacyDfuException` hierarchy.
- [x] **FW-T025**: Implement `LegacyDfuTransport`
`feature/firmware/src/commonMain/.../ota/dfu/LegacyDfuTransport.kt`
Full Nordic Legacy DFU (1530/Adafruit BLEDfu): DFU-mode connect, DFU version gate, init-packet bracket, firmware streaming with PRN, OTAFIX-2.1 high-MTU detection, connection-drop watcher.
- [x] **FW-T026**: Define `DfuUploadTransport` interface
`feature/firmware/src/commonMain/.../ota/dfu/DfuUploadTransport.kt`
Common upload surface: `connectToDfuMode()`, `transferInitPacket()`, `transferFirmware()`, `abort()`, `close()`.
- [x] **FW-T027**: Implement `DfuZipParser`
`feature/firmware/src/commonMain/.../ota/dfu/DfuZipParser.kt`
Parses pre-extracted zip entries into `DfuZipPackage` (manifest.json → .dat + .bin).
- [x] **FW-T028**: Implement `SecureDfuHandler`
`feature/firmware/src/commonMain/.../ota/dfu/SecureDfuHandler.kt`
nRF52 DFU orchestrator: obtain zip → extract .dat/.bin → disconnect mesh → trigger buttonless → detect protocol (Secure vs Legacy) → connect → transfer → validate → report success.
### USB / UF2
- [x] **FW-T029**: Implement `UsbUpdateHandler`
`feature/firmware/src/commonMain/.../UsbUpdateHandler.kt`
USB/UF2 handler delegating to `performUsbUpdate()`.
- [x] **FW-T030**: Implement `performUsbUpdate()` shared logic
`feature/firmware/src/commonMain/.../UsbUpdateSupport.kt`
Download UF2 → reboot to DFU → present `AwaitingFileSave` state. Handles both download and local-file paths.
- [x] **FW-T031**: Define `FirmwareUsbManager` interface
`feature/firmware/src/commonMain/.../FirmwareUsbManager.kt`
`deviceDetachFlow()` — emits when the USB device disconnects after flashing.
- [x] **FW-T032**: Implement platform USB managers
`feature/firmware/src/androidMain/.../AndroidFirmwareUsbManager.kt`
`feature/firmware/src/jvmMain/.../DesktopFirmwareUsbManager.kt`
---
## Phase 4 — ViewModel & UI
- [x] **FW-T033**: Implement `DefaultFirmwareUpdateManager`
`feature/firmware/src/commonMain/.../DefaultFirmwareUpdateManager.kt`
Handler router: BLE+ESP32→OTA, BLE+nRF52→DFU, TCP+ESP32→OTA, Serial+nRF52→USB. Target address resolution.
- [x] **FW-T034**: Implement `FirmwareUpdateViewModel`
`feature/firmware/src/commonMain/.../FirmwareUpdateViewModel.kt`
Orchestrates `checkForUpdates()`, `startUpdate()`, `startUpdateFromFile()`, `saveDfuFile()`, `cancelUpdate()`, `dismissBootloaderWarningForCurrentDevice()`. Post-update verification with 60-second timeout. Battery check (≤10%). Temp file cleanup via `ApplicationCoroutineScope` + `NonCancellable`.
- [x] **FW-T035**: Implement `FirmwareUpdateScreen`
`feature/firmware/src/commonMain/.../FirmwareUpdateScreen.kt`
Full CMP UI: `FirmwareUpdateScaffold`, `ReleaseTypeSelector`, `DeviceInfoCard`, `ReadyState`, `ProgressContent`, `VerifyingState`, `VerificationFailedState`, `ErrorState`, `SuccessState`, `AwaitingFileSaveState`, `DisclaimerDialog`, `BootloaderWarningCard`, `ChirpyCard`, `CyclingMessages`, `KeepScreenOn`, file picker and save launchers.
- [x] **FW-T036**: Implement `FirmwareNavigation`
`feature/firmware/src/commonMain/.../navigation/FirmwareNavigation.kt`
Navigation 3 `firmwareGraph()` registering `FirmwareRoute.FirmwareGraph` and `FirmwareRoute.FirmwareUpdate`.
- [x] **FW-T037**: Implement `FeatureFirmwareModule` (DI)
`feature/firmware/src/commonMain/.../di/FeatureFirmwareModule.kt`
Koin module with `@ComponentScan("org.meshtastic.feature.firmware")`.
---
## Phase 5 — Testing
- [x] **FW-T038**: ViewModel unit tests
`feature/firmware/src/commonTest/.../FirmwareUpdateViewModelTest.kt`
13 tests: initialization, release type switching, battery check, update success/error, cancel, bootloader warning, update method detection.
- [x] **FW-T039**: Integration tests
`feature/firmware/src/commonTest/.../FirmwareUpdateIntegrationTest.kt`
4 tests: end-to-end ViewModel state transitions with real ViewModel + fake/mock collaborators.
- [x] **FW-T040**: Handler routing tests
`feature/firmware/src/commonTest/.../DefaultFirmwareUpdateManagerTest.kt`
12 tests: BLE/Serial/TCP × ESP32/nRF52 routing, target resolution, error cases.
- [x] **FW-T041**: Firmware retriever tests
`feature/firmware/src/commonTest/.../CommonFirmwareRetrieverTest.kt`
11 tests: manifest resolution, current/legacy naming fallback, zip extraction, version stripping, platformioTarget vs hwModelSlug.
- [x] **FW-T042**: Firmware file validation tests
`feature/firmware/src/commonTest/.../IsValidFirmwareFileTest.kt`
13 tests: valid firmware names, exclusion patterns (littlefs, bleota, mt-, factory), wrong extension, target mismatch, edge cases.
- [x] **FW-T043**: State model tests
`feature/firmware/src/commonTest/.../FirmwareUpdateStateTest.kt`
4 tests: ProgressState defaults, stripFormatArgs variations.
- [x] **FW-T044**: Manifest deserialization tests
`feature/firmware/src/commonTest/.../FirmwareManifestTest.kt`
- [x] **FW-T045**: USB update flow tests
`feature/firmware/src/commonTest/.../CommonPerformUsbUpdateTest.kt`
- [x] **FW-T046**: BLE OTA transport tests
`feature/firmware/src/commonTest/.../ota/BleOtaTransportTest.kt`
- [x] **FW-T047**: BLE scan support tests
`feature/firmware/src/commonTest/.../ota/BleScanSupportTest.kt`
MAC+1 calculation, scan retry logic.
- [x] **FW-T048**: OTA response parsing tests
`feature/firmware/src/commonTest/.../ota/OtaResponseTest.kt`
- [x] **FW-T049**: Throughput tracker tests
`feature/firmware/src/commonTest/.../ota/ThroughputTrackerTest.kt`
- [x] **FW-T050**: SHA-256 hash tests
`feature/firmware/src/commonTest/.../ota/FirmwareHashUtilTest.kt`
- [x] **FW-T051**: DFU CRC-32 tests
`feature/firmware/src/commonTest/.../ota/dfu/DfuCrc32Test.kt`
- [x] **FW-T052**: DFU response parsing tests
`feature/firmware/src/commonTest/.../ota/dfu/DfuResponseTest.kt`
- [x] **FW-T053**: DFU zip parser tests
`feature/firmware/src/commonTest/.../ota/dfu/DfuZipParserTest.kt`
- [x] **FW-T054**: Legacy DFU protocol tests
`feature/firmware/src/commonTest/.../ota/dfu/LegacyDfuProtocolTest.kt`
- [x] **FW-T055**: Legacy DFU transport tests
`feature/firmware/src/commonTest/.../ota/dfu/LegacyDfuTransportTest.kt`
- [x] **FW-T056**: Secure DFU protocol tests
`feature/firmware/src/commonTest/.../ota/dfu/SecureDfuProtocolTest.kt`
- [x] **FW-T057**: Secure DFU transport tests
`feature/firmware/src/commonTest/.../ota/dfu/SecureDfuTransportTest.kt`
- [x] **FW-T058**: JVM ViewModel file tests
`feature/firmware/src/jvmTest/.../FirmwareUpdateViewModelFileTest.kt`
---
## Identified Gaps
- [x] **FW-T059**: Add `WifiOtaTransport` unit tests
The WiFi/TCP OTA transport has no dedicated test coverage. Should test connection, command sending, response reading, firmware streaming, and error handling using a fake Ktor socket.
- [ ] **[DEFERRED]** **FW-T060**: Add `FirmwareUpdateScreen` composable/screenshot tests — *Deferred: requires Compose UI test infrastructure.*
No UI tests exist for the 889-line screen composable. Should test at minimum: Ready state rendering, progress state rendering, error state rendering, and success state rendering.
- [ ] **[DEFERRED]** **FW-T061**: Add `Esp32OtaUpdateHandler` unit tests — *Deferred: requires integration test harness for TCP OTA protocol — mock dispatchers incompatible with real sockets.*
The ESP32 OTA handler orchestration logic (firmware retrieval → hash → reboot → connect → stream) has no isolated test. Currently only covered by proxy through integration tests.
---
## Summary
| Category | Count | Status |
|----------|-------|--------|
| Completed tasks | 58 | ✅ All done |
| Gap tasks | 3 | ⬜ Open |
| **Total** | **61** | — |
| Phase | Tasks | Status |
|-------|-------|--------|
| 1. Core Models & Interfaces | FW-T001FW-T007 | ✅ Complete |
| 2. Firmware Retrieval | FW-T008FW-T013 | ✅ Complete |
| 3. OTA & DFU Protocols | FW-T014FW-T032 | ✅ Complete |
| 4. ViewModel & UI | FW-T033FW-T037 | ✅ Complete |
| 5. Testing | FW-T038FW-T058 | ✅ Complete |
| Gaps | FW-T059FW-T061 | ⬜ Open |

View File

@@ -1,220 +0,0 @@
# Implementation Plan: Node Detail & Metrics
**Branch**: `007-node-detail-metrics` | **Date**: 2025-07-15 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/007-node-detail-metrics/spec.md`
**Note**: This is a brownfield migration — all implementation is complete. This plan documents the architecture as-built.
## Summary
The Node Detail & Metrics feature provides per-node inspection with nine metric log screens, interactive Vico charts, time-frame filtering, CSV export, compass-based navigation, and remote administration. All code resides in `commonMain` using Compose Multiplatform, Koin DI, and Navigation 3 with Material 3 Adaptive ListDetailSceneStrategy.
## Technical Context
**Language/Version**: Kotlin 2.3+ targeting JDK 21
**Primary Dependencies**: Compose Multiplatform, Material 3 Adaptive, Koin 4.2+ (K2 Compiler Plugin), Vico (Patrykandpatrick) charting, Coil 3 (async image), Okio (CSV/Base64)
**Storage**: Room KMP for mesh logs and nodes, DataStore KMP for user preferences (display units, Fahrenheit)
**Testing**: KMP `allTests` for `feature:node` — Mokkery mocking, Turbine flow testing
**Target Platform**: Android, Desktop (JVM), iOS — all via `commonMain`
**Project Type**: Mobile/desktop app (Kotlin Multiplatform)
**Performance Goals**: 60fps chart scrolling, <100ms chart↔card sync
**Constraints**: All UI in `commonMain`; no `java.*`/`android.*` in common; CMP float pre-formatting via `NumberFormatter.format()` / `MetricFormatter`
**Scale/Scope**: ~70 source files, ~13,000 lines (feature/node commonMain, excluding list layout); ~14 test files, ~2,000 test lines
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Kotlin Multiplatform Core | ✅ PASS | All code in `commonMain`. Compass providers use `expect`/`actual`. No `java.*`/`android.*` imports in common. |
| II. Zero Lint Tolerance | ✅ PASS | `spotlessApply` + `detekt` pass. Suppressions documented (`MagicNumber`, `LongMethod`, `CyclomaticComplexMethod`). |
| III. Compose Multiplatform UI | ✅ PASS | CMP composables throughout. `NumberFormatter.format()` for all floats. Navigation 3 with `ListDetailSceneStrategy`. |
| IV. Privacy First | ✅ PASS | No PII logging. Public keys displayed but never transmitted. Hardware images fetched from public CDN. |
| V. Design Standards Compliance | ✅ PASS | M3 components: `SectionCard`, `ListItem`, `SwitchListItem`, `FilterChip`, `AssistChip`. TalkBack semantics on key elements. |
| VI. Verify Before Push | ✅ PASS | Full verification pipeline: `./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests`. |
| VII. Coroutine Safety | ✅ PASS | `safeLaunch {}` used throughout. `dispatchers.io` (project injected), not `Dispatchers.IO`. |
| VIII. Resource Discipline | ✅ PASS | `stringResource(Res.string.key)` everywhere. `MeshtasticIcons` for all icons. |
| IX. Branch & Scope Hygiene | ✅ PASS | Feature scoped to `feature/node` module with well-defined sub-packages. |
**Gate Result**: ✅ All principles satisfied
## Project Structure
### Documentation (this feature)
```text
specs/007-node-detail-metrics/
├── spec.md # Feature specification (migrated)
├── plan.md # This file (migrated)
└── tasks.md # Task breakdown (migrated)
```
### Source Code (repository root)
```text
feature/node/
├── src/commonMain/kotlin/org/meshtastic/feature/node/
│ ├── compass/
│ │ ├── CompassHeadingProvider.kt ← expect declaration
│ │ ├── CompassUiState.kt ← Compass UI state model
│ │ ├── CompassViewModel.kt ← Heading, bearing, distance, true-north
│ │ ├── MagneticFieldProvider.kt ← expect declaration
│ │ └── PhoneLocationProvider.kt ← expect declaration
│ ├── component/
│ │ ├── AdministrationSection.kt ← Remote admin + firmware section
│ │ ├── ChannelInfo.kt ← Channel display
│ │ ├── CompassBottomSheet.kt ← Compass sheet composable
│ │ ├── CooldownOutlinedIconButton.kt ← Request cooldown button
│ │ ├── DeviceActions.kt ← DM, share, favorite, ignore, mute, remove
│ │ ├── DeviceDetailsSection.kt ← Hardware model + support status
│ │ ├── DistanceInfo.kt ← Distance display
│ │ ├── ElevationInfo.kt ← Altitude display
│ │ ├── EnvironmentMetrics.kt ← Inline env metrics summary
│ │ ├── FirmwareReleaseSheetContent.kt ← Firmware release bottom sheet
│ │ ├── HopsInfo.kt ← Hop count display
│ │ ├── IconInfo.kt ← Reusable icon+text pair
│ │ ├── InfoCard.kt ← Section card wrapper
│ │ ├── LastHeardInfo.kt ← Last heard display
│ │ ├── LinkedCoordinatesItem.kt ← Clickable coordinates
│ │ ├── NodeContextMenu.kt ← Context menu
│ │ ├── NodeDetailComponentPreviews.kt ← Preview composables
│ │ ├── NodeDetailComponents.kt ← Shared UI primitives
│ │ ├── NodeDetailsSection.kt ← Identity card
│ │ ├── NodeMenuAction.kt ← Sealed action types
│ │ ├── NodeStatusIcons.kt ← Status indicators
│ │ ├── NotesSection.kt ← Editable notes
│ │ ├── PositionSection.kt ← Inline map + compass button
│ │ ├── PowerMetrics.kt ← Inline power summary
│ │ ├── SatelliteCountInfo.kt ← Satellite count
│ │ ├── TelemetricActionsSection.kt ← Telemetry feature rows
│ │ └── TelemetryInfo.kt ← Telemetry display
│ ├── detail/
│ │ ├── CommonNodeRequestActions.kt ← Shared request action impls
│ │ ├── HandleNodeAction.kt ← Action dispatch router
│ │ ├── NodeDetailActions.kt ← Coordinated action facade
│ │ ├── NodeDetailContent.kt ← Crossfade + LazyColumn detail
│ │ ├── NodeDetailPreviews.kt ← Preview composables
│ │ ├── NodeDetailScreens.kt ← Scaffold + overlay management
│ │ ├── NodeDetailViewModel.kt ← Node detail ViewModel
│ │ ├── NodeManagementActions.kt ← CRUD node actions
│ │ └── NodeRequestActions.kt ← Telemetry/traceroute requests
│ ├── di/
│ │ └── FeatureNodeModule.kt ← Koin module
│ ├── domain/usecase/
│ │ ├── CommonGetNodeDetailsUseCase.kt ← Shared use case impl
│ │ ├── GetFilteredNodesUseCase.kt ← Node list filtering (spec 002)
│ │ └── GetNodeDetailsUseCase.kt ← Node detail aggregator
│ ├── metrics/
│ │ ├── BaseMetricChart.kt ← GenericMetricChart, BaseMetricScreen, AdaptiveLayout
│ │ ├── ChartStyling.kt ← Line styles, markers, threshold lines
│ │ ├── CommonCharts.kt ← Shared chart helpers (time axis, scroll)
│ │ ├── DeviceMetrics.kt ← Device metric screen + chart + card
│ │ ├── EnvironmentCharts.kt ← Environment multi-series chart
│ │ ├── EnvironmentMetrics.kt ← Environment metric screen + card
│ │ ├── EnvironmentMetricsState.kt ← Environment graphing data model
│ │ ├── HardwareModelExtensions.kt ← Safe hardware model number lookup
│ │ ├── HostMetricsChart.kt ← Host metrics chart
│ │ ├── HostMetricsLog.kt ← Host metrics screen + card
│ │ ├── MetricLogComponents.kt ← Shared metric UI (indicators, legends, dialogs)
│ │ ├── MetricsViewModel.kt ← Central metrics ViewModel
│ │ ├── NeighborInfoLog.kt ← Neighbor info log screen
│ │ ├── PaxMetrics.kt ← Pax metrics screen + chart + card
│ │ ├── PositionLogComponents.kt ← Position card composable
│ │ ├── PositionLogScreens.kt ← Position log screen
│ │ ├── PowerMetrics.kt ← Power metrics screen + chart + card
│ │ ├── SignalMetrics.kt ← Signal metrics screen + chart + card
│ │ ├── TimeFrameSelector.kt ← Time-frame chip selector
│ │ ├── TracerouteChart.kt ← Traceroute chart + data model
│ │ └── TracerouteLog.kt ← Traceroute log screen + card
│ ├── model/
│ │ ├── IsEffectivelyUnmessageable.kt ← Node messaging capability check
│ │ ├── LogsType.kt ← Log type enum with routes
│ │ ├── MetricInfo.kt ← Metric display info
│ │ ├── MetricsState.kt ← Aggregate metric state
│ │ ├── NodeDetailAction.kt ← Sealed detail actions
│ │ └── TimeFrame.kt ← Time window enum
│ └── navigation/
│ ├── AdaptiveNodeListScreen.kt ← Adaptive list/detail (spec 002)
│ └── NodesNavigation.kt ← Nav3 graph entries
├── src/commonTest/kotlin/org/meshtastic/feature/node/
│ ├── compass/CompassViewModelTest.kt
│ ├── detail/HandleNodeActionTest.kt
│ ├── detail/NodeDetailViewModelTest.kt
│ ├── detail/NodeManagementActionsTest.kt
│ ├── domain/usecase/GetFilteredNodesUseCaseTest.kt
│ ├── list/NodeListViewModelTest.kt
│ ├── metrics/DecodePaxFromLogTest.kt
│ ├── metrics/EnvironmentMetricsForGraphingTest.kt
│ ├── metrics/EnvironmentMetricsStateTest.kt
│ ├── metrics/FormatBytesTest.kt
│ ├── metrics/HardwareModelSafeNumberTest.kt
│ ├── metrics/MetricsViewModelTest.kt
│ ├── metrics/TracerouteChartTest.kt
│ └── model/TimeFrameTest.kt
```
**Structure Decision**: All code lives in `feature/node` module, organised by concern (detail, metrics, compass, component, model, navigation). Metrics screens share `BaseMetricScreen` and `GenericMetricChart` to avoid duplication.
## Module Impact
| Module | Change Type | Files Affected | Risk |
|--------|-------------|----------------|------|
| `feature/node` | Primary | ~70 source + ~14 test | Low (self-contained) |
| `core/model` | Read-only dep | 0 modified | None |
| `core/ui` | Read-only dep | 0 modified | None |
| `core/resources` | String additions | strings.xml | Low |
| `core/navigation` | Route definitions used | 0 modified | None |
| `core/database` | Entities read | 0 modified | None |
| `core/repository` | Repositories injected | 0 modified | None |
## Integration Points
- **Navigation**: `NodesNavigation.nodesGraph()` registers all routes using Navigation 3 `entry<T>` with `ListDetailSceneStrategy` pane metadata.
- **DI**: `FeatureNodeModule` provides `NodeDetailViewModel`, `MetricsViewModel`, `CompassViewModel` via `@KoinViewModel`.
- **DataStore**: Display units and Fahrenheit preference read from user settings DataStore.
- **Room**: Mesh logs, node DB, and traceroute snapshot positions queried via repositories.
- **Service**: `ServiceRepository` provides live traceroute responses and accepts `ServiceAction` commands.
## Design Constraints
- All UI lives in `commonMain` — not platform-specific
- Strings accessed via `stringResource(Res.string.key)` — never hardcoded
- Icons use `MeshtasticIcons` exclusively (from `core/ui/icon/`)
- Error handling uses `safeLaunch {}` / `safeCatching {}` not `runCatching {}`
- Dispatchers via `org.meshtastic.core.di.CoroutineDispatchers` (injected)
- Float values must be pre-formatted with `NumberFormatter.format()` / `MetricFormatter` (CMP constraint)
- Vico chart x-step minimum of 60 seconds to prevent slot-count explosion
## Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| Vico chart performance with 1000+ data points | Low | Medium | Time-frame filtering limits visible data; x-step floor prevents micro-slotting |
| Compass heading drift without magnetometer | Medium | Low | `NO_MAGNETOMETER` warning displayed to user |
| Traceroute map unavailability (no positioned nodes) | Medium | Low | `evaluateTracerouteMapAvailability` checks before navigation, shows error dialog |
## Phase Alignment with Tasks
| Phase | Purpose | Key Tasks | Dependencies |
|-------|---------|-----------|--------------|
| 1. Data Layer | Models + state | NDM-T001T003 | None |
| 2. Detail Screen | Node detail sections | NDM-T004T010 | Phase 1 |
| 3. Chart Infrastructure | BaseMetricScreen + Vico | NDM-T011T013 | Phase 1 |
| 4. Metric Screens | Nine individual screens | NDM-T014T022 | Phase 3 |
| 5. Compass | Heading + bearing | NDM-T023T024 | Phase 1 |
| 6. Navigation | Nav3 graph | NDM-T025 | Phase 2, 4 |
| 7. Testing | Unit + ViewModel tests | NDM-T026T033 | All prior |
### Critical Path
```
Phase 1 → Phase 2 + Phase 3 → Phase 4 → Phase 5 → Phase 6 → Phase 7
```
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| *None* | — | — |

View File

@@ -1,304 +0,0 @@
# Feature Specification: Node Detail & Metrics
**Feature Branch**: `007-node-detail-metrics`
**Created**: 2025-07-15
**Status**: Migrated
**Input**: Brownfield migration of existing `feature/node` detail screens, metrics charts, compass, and telemetry request actions (excluding node list layout covered by spec 002).
## Summary
The Node Detail & Metrics feature provides a comprehensive per-node inspection experience, allowing users to view a node's identity, hardware details, firmware status, telemetry data, position, and network diagnostics. It aggregates nine distinct metric log screens — device, environment, signal, power, host, pax, traceroute, neighbor info, and position — each with interactive Vico charts, time-frame filtering, CSV export, and card-to-chart synchronisation. A compass bottom-sheet offers bearing/distance guidance toward a target node using the phone's sensors.
## Goals
1. **G-001**: Provide a single, scrollable node detail screen showing identity (name, role, node ID, public key), signal metrics (SNR, RSSI), hops, uptime, and MQTT/PKC status.
2. **G-002**: Deliver nine dedicated metric log screens with interactive line charts (Vico), time-frame filtering (1h → all-time), and bi-directional chart↔card selection sync.
3. **G-003**: Support CSV export for device, environment, signal, power, and position metrics.
4. **G-004**: Enable on-demand telemetry requests (device, environment, signal, power, host, pax, air quality) and network diagnostics (traceroute, neighbor info) with cooldown-guarded buttons.
5. **G-005**: Provide compass-based bearing/distance guidance toward a target node with true-north correction, positional accuracy, and real-time heading updates.
## Non-Goals
- **NG-001**: Node list layout, sorting, filtering, and density settings (covered by spec 002 — `NodeItem`, `NodeItemCompact`, `NodeListDensity`, `NodeListHelp`, `NodeLayoutSettings`).
- **NG-002**: Channel/radio configuration UI (handled by `feature/settings`).
- **NG-003**: Map rendering implementation (provided by platform-specific `LocalInlineMapProvider` / `LocalTracerouteMapScreenProvider`).
- **NG-004**: Full firmware OTA update flow (covered by spec 006).
## User Scenarios & Testing *(mandatory)*
### User Story 1 — View Node Details (Priority: P1)
A user taps a node in the node list to see its full identity, hardware details, signal quality, and firmware version on a single scrollable screen.
**Why this priority**: Core discovery — users need to inspect any node's metadata before taking further action.
**Independent Test**: Navigating to a node detail screen and verifying that all sections (details, device, notes, administration, firmware) render correctly with real or mock data.
**Acceptance Scenarios**:
1. **Given** a node with a valid position and PKC key, **When** the user navigates to the detail screen, **Then** the NodeDetailsSection displays short name, role, node ID, node number, last heard, hops away, user ID, uptime, SNR, RSSI, via MQTT status, and public key.
2. **Given** a node with known hardware, **When** the DeviceDetailsSection renders, **Then** it shows the hardware model image, display name, PlatformIO target, and support status.
3. **Given** a remote node with metadata, **When** the AdministrationSection renders, **Then** it shows session status (NoSession / Active / Stale), remote-admin button, refresh metadata, firmware edition, installed version, latest stable, and latest alpha with colour-coded version comparison.
4. **Given** a node with a mismatched encryption key, **When** the detail screen loads, **Then** the MismatchKeyWarning is displayed in the details section.
---
### User Story 2 — View & Export Device Metrics (Priority: P1)
A user views battery level, voltage, channel utilisation, air utilisation, and uptime over time as interactive charts and card lists, and optionally exports the data as CSV.
**Why this priority**: Battery and channel metrics are the most commonly monitored telemetry values.
**Independent Test**: Navigate to DeviceMetricsScreen, verify chart renders with 4 series (battery, voltage, ch util, air util), select a card to highlight the corresponding chart point, change the time frame, and export CSV.
**Acceptance Scenarios**:
1. **Given** device telemetry data for a node, **When** the user opens Device Metrics, **Then** a dual-axis Vico chart is shown (percent on left, voltage on right) with threshold line at 20% battery.
2. **Given** the user selects a metric card, **When** the card is clicked, **Then** the chart scrolls and highlights the corresponding data point, and vice-versa.
3. **Given** data spanning 8 days, **When** the TimeFrameSelector is shown, **Then** only time frames that fit the data range are enabled (1h, 24h, 1 week; not 2 weeks or 1 month).
4. **Given** device metric data, **When** the user taps the save icon, **Then** a CSV file is written with date, time, batteryLevel, voltage, channelUtilization, airUtilTx, and uptimeSeconds columns.
---
### User Story 3 — View Environment Metrics (Priority: P1)
A user views temperature, humidity, barometric pressure, soil metrics, wind, rainfall, IAQ, gas resistance, lux, UV lux, voltage, current, radiation, and one-wire temperature sensors.
**Why this priority**: Environment sensors are a primary use-case for Meshtastic sensor networks.
**Independent Test**: Navigate to EnvironmentMetricsScreen with a full set of environment telemetry and verify all sub-displays render; toggle Fahrenheit to verify temperature unit conversion.
**Acceptance Scenarios**:
1. **Given** environment telemetry with temperature data, **When** `isFahrenheit` is true, **Then** temperatures are converted from Celsius to Fahrenheit in both the chart and the cards.
2. **Given** environment telemetry with one-wire sensors, **When** the card renders, **Then** up to 8 one-wire temperature readings are displayed with individual color indicators.
3. **Given** environment data, **When** the user exports, **Then** the CSV includes all environment fields including the 8 one-wire columns.
---
### User Story 4 — View Signal Metrics (Priority: P2)
A user views RSSI and SNR for packets received from a node over time with a dual-axis chart and LoRa signal quality indicator.
**Why this priority**: Signal quality is critical for mesh network optimisation.
**Independent Test**: Open SignalMetricsScreen, verify RSSI (left axis) and SNR (right axis) chart rendering with LoraSignalIndicator in each card.
**Acceptance Scenarios**:
1. **Given** signal metrics from a node, **When** displayed, **Then** the chart shows RSSI on the left axis and SNR on the right axis with distinct colours and line styles (solid vs dashed).
2. **Given** a signal card, **When** rendered, **Then** it includes a LoraSignalIndicator widget showing signal quality derived from SNR and RSSI.
---
### User Story 5 — Traceroute & Network Diagnostics (Priority: P2)
A user runs a traceroute to a remote node to discover the mesh path, view forward/return hops, round-trip time, and optionally view results on a map.
**Why this priority**: Network path discovery is key for troubleshooting mesh connectivity.
**Independent Test**: Trigger a traceroute, wait for a response, verify the TracerouteLogScreen shows matched request/response pairs with hop counts and RTT, and tap "View on Map".
**Acceptance Scenarios**:
1. **Given** a traceroute request and matching response, **When** `resolveTraceroutePoints` runs, **Then** the result contains forward hops, return hops (if available), and round-trip seconds.
2. **Given** a traceroute point, **When** the user taps a card, **Then** a detail dialog shows annotated forward/return routes with colour-coded node names and a "View on Map" button.
3. **Given** no matching response, **When** the card renders, **Then** it shows "No response" with a PersonOff icon and null hop/RTT values.
4. **Given** traceroute results, **When** the user views the chart, **Then** forward hops (blue), return hops (green), and RTT (orange) are displayed as separate line series.
---
### User Story 6 — Neighbor Info (Priority: P2)
A user requests neighbor info from a node to see which nodes it can directly hear.
**Why this priority**: Neighbor info complements traceroute for mesh topology understanding.
**Independent Test**: Request neighbor info, verify NeighborInfoLogScreen shows annotated results with colour-coded signal quality.
**Acceptance Scenarios**:
1. **Given** a neighbor info response, **When** displayed, **Then** the result is shown with annotated, colour-coded neighbor entries.
2. **Given** a cooldown period on the request button, **When** the button was recently pressed, **Then** it is disabled until cooldown expires.
---
### User Story 7 — Power, Host, and Pax Metrics (Priority: P3)
A user views power channel voltage/current (up to 8 channels), host system load/memory/disk, and paxcount (BLE+WiFi device counts) on dedicated metric screens.
**Why this priority**: Specialised sensor data for advanced use cases.
**Independent Test**: Open each screen and verify chart/card rendering with time-frame filtering.
**Acceptance Scenarios**:
1. **Given** power metrics with 3 active channels, **When** PowerMetricsScreen opens, **Then** a channel selector chip row appears and the chart updates per selected channel.
2. **Given** host metrics, **When** HostMetricsLogScreen renders, **Then** load averages show coloured progress bars and free memory/disk are formatted with human-readable byte strings.
3. **Given** pax metrics, **When** PaxMetricsScreen renders, **Then** the chart shows three series (total, BLE, WiFi) and cards display total, BLE, WiFi, and uptime.
---
### User Story 8 — Position Log & Compass (Priority: P2)
A user views historical GPS positions on a map and in a card list, and opens a bearing compass toward a target node.
**Why this priority**: Position tracking is central to outdoor mesh deployments.
**Independent Test**: Open PositionLogScreen, verify map integration and card list; open compass bottom-sheet and verify heading, bearing, distance, and warning states.
**Acceptance Scenarios**:
1. **Given** position logs for a node, **When** the user opens Position Log, **Then** a map shows the node's track and a card list shows each position with coordinates, altitude, speed, heading, and satellite count.
2. **Given** a target node with a valid position, **When** the user opens the compass, **Then** heading, bearing, distance, and alignment indicator are shown with true-north correction applied.
3. **Given** no magnetometer sensor, **When** the compass opens, **Then** a `NO_MAGNETOMETER` warning is displayed.
4. **Given** position data, **When** the user taps the save icon, **Then** a CSV file includes latitude, longitude, altitude, satsInView, speed, and heading.
---
### User Story 9 — Node Management Actions (Priority: P2)
A user manages a node by sending direct messages, sharing its contact QR code, favoriting, muting, ignoring, or removing it from the local database.
**Why this priority**: Node management is a secondary but essential user flow from the detail screen.
**Independent Test**: From the detail screen, favorite/unfavorite a node, toggle ignore/mute, tap "Direct Message", share contact, and remove the node.
**Acceptance Scenarios**:
1. **Given** a remote node that is not effectively unmessageable, **When** the detail screen renders, **Then** a "Direct Message" button is shown.
2. **Given** the user taps the favorite toggle, **When** the action is dispatched, **Then** `NodeManagementActions.requestFavoriteNode` is called.
3. **Given** the user taps "Remove", **When** confirmed, **Then** the node is removed from the local database and the user is navigated back.
---
### User Story 10 — Remote Administration (Priority: P3)
A user opens remote admin for a node, with passkey session management and status feedback.
**Why this priority**: Advanced feature for power users managing remote mesh nodes.
**Independent Test**: Open remote admin on a connected node, verify session status transitions (NoSession → Active), and handle timeout/disconnection snackbars.
**Acceptance Scenarios**:
1. **Given** a connected radio, **When** the user taps Remote Admin, **Then** `ensureRemoteAdminSession` is called and on success, the app navigates to `SettingsRoute.Settings(destNum)`.
2. **Given** a disconnected radio, **When** remote admin is attempted, **Then** a snackbar shows "Connect radio for remote admin".
3. **Given** session timeout, **When** remote admin is attempted, **Then** a snackbar shows "Remote admin unreachable".
---
### Edge Cases
- What happens when a node has no telemetry data? → Metric screens show empty charts and empty card lists.
- What happens when environment metrics have `NaN` values? → Individual displays guard with `isNaN()` checks and skip rendering.
- What happens when `paxcount` has all-zero values? → `decodePaxFromLog` returns null, filtering out the entry.
- What happens when traceroute sub-second precision timestamps are used? → `timeSeconds` truncates to whole seconds to prevent Vico crashes.
- What happens when `soil_moisture` is `Int.MIN_VALUE`? → The sentinel value is filtered out in `SoilMetricsDisplay`.
## Architecture
### Key Components
| Component | Module / File | Purpose |
|-----------|---------------|---------|
| `NodeDetailScreen` | `feature/node/detail/NodeDetailScreens.kt` | Top-level detail scaffold with nav, overlays, and action routing |
| `NodeDetailContent` | `feature/node/detail/NodeDetailContent.kt` | Crossfade loading → scrollable detail list |
| `NodeDetailViewModel` | `feature/node/detail/NodeDetailViewModel.kt` | Coordinates node identity, metrics, session status |
| `MetricsViewModel` | `feature/node/metrics/MetricsViewModel.kt` | Manages metric data, time-frame filtering, CSV export, traceroute overlay cache |
| `BaseMetricScreen` | `feature/node/metrics/BaseMetricChart.kt` | Generic scaffold for all metric screens (AppBar, adaptive layout, chart↔list sync) |
| `GenericMetricChart` | `feature/node/metrics/BaseMetricChart.kt` | Vico CartesianChartHost wrapper with markers, FadingEdges, zoom |
| `DeviceMetricsScreen` | `feature/node/metrics/DeviceMetrics.kt` | Battery, voltage, channel util, air util chart + cards |
| `EnvironmentMetricsScreen` | `feature/node/metrics/EnvironmentMetrics.kt` | Full environment sensor display |
| `SignalMetricsScreen` | `feature/node/metrics/SignalMetrics.kt` | RSSI + SNR dual-axis chart |
| `PowerMetricsScreen` | `feature/node/metrics/PowerMetrics.kt` | Multi-channel power voltage/current |
| `TracerouteLogScreen` | `feature/node/metrics/TracerouteLog.kt` | Traceroute request/response pairing, hop chart, map integration |
| `PositionLogScreen` | `feature/node/metrics/PositionLogScreens.kt` | Position track map + card list |
| `HostMetricsLogScreen` | `feature/node/metrics/HostMetricsLog.kt` | Linux host load/memory/disk |
| `PaxMetricsScreen` | `feature/node/metrics/PaxMetrics.kt` | BLE + WiFi paxcount chart |
| `NeighborInfoLogScreen` | `feature/node/metrics/NeighborInfoLog.kt` | Neighbor discovery log |
| `CompassViewModel` | `feature/node/compass/CompassViewModel.kt` | Heading, bearing, distance, true-north correction |
| `NodeDetailsSection` | `feature/node/component/NodeDetailsSection.kt` | Identity card (name, role, ID, hops, signal, PKC) |
| `DeviceDetailsSection` | `feature/node/component/DeviceDetailsSection.kt` | Hardware model image, support status |
| `DeviceActions` | `feature/node/component/DeviceActions.kt` | DM, share, favorite, ignore, mute, remove |
| `TelemetricActionsSection` | `feature/node/component/TelemetricActionsSection.kt` | Telemetry request buttons + inline log navigation |
| `AdministrationSection` | `feature/node/component/AdministrationSection.kt` | Remote admin session + firmware version info |
| `TimeFrame` | `feature/node/model/TimeFrame.kt` | Enum of 1h → all-time windows with threshold calculation |
| `MetricsState` | `feature/node/model/MetricsState.kt` | Aggregate state for all metric types + hardware |
| `NodesNavigation` | `feature/node/navigation/NodesNavigation.kt` | Nav3 graph entries with ListDetail pane strategy |
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST display node identity (short name, role, node ID, node number, last heard, hops away, user ID, uptime) in a structured detail section.
- **FR-002**: System MUST display SNR and RSSI for directly-heard nodes (hops == 0 and not viaMqtt).
- **FR-003**: System MUST display public key as base64, with long-press to copy; display "Error" for all-zero 32-byte keys.
- **FR-004**: System MUST render nine distinct metric log screens (device, environment, signal, power, host, pax, traceroute, neighbor info, position).
- **FR-005**: Each metric screen MUST support time-frame filtering with `TimeFrame` enum (1h, 24h, 1 week, 2 weeks, 1 month, all time).
- **FR-006**: Time-frame options MUST be dynamically filtered based on the oldest available data point.
- **FR-007**: Metric charts MUST use Vico `CartesianChartHost` with dual-axis support and `FadingEdges`.
- **FR-008**: Selecting a chart point MUST scroll the card list to the matching item, and vice-versa.
- **FR-009**: System MUST support CSV export for device, environment, signal, power, and position metrics.
- **FR-010**: Telemetry request buttons MUST enforce cooldown periods to prevent spamming the mesh.
- **FR-011**: Traceroute results MUST be paired with requests by packet ID and display forward hops, return hops, and round-trip seconds.
- **FR-012**: Compass MUST apply true-north correction using magnetic declination from the phone's location.
- **FR-013**: Compass MUST calculate positional accuracy from GPS accuracy + DOP or precision bits.
- **FR-014**: Environment metrics MUST convert temperatures to Fahrenheit when `isFahrenheit` is true.
- **FR-015**: Remote admin MUST ensure a fresh session passkey before navigating to settings.
- **FR-016**: System MUST display hardware model image loaded from `flasher.meshtastic.org` with fallback placeholder.
- **FR-017**: Firmware version MUST be colour-coded (green = latest stable, yellow = between stable and alpha, orange = above alpha, red = below stable).
- **FR-018**: System MUST display device actions (DM, share contact, favorite, ignore, mute, remove) for remote nodes.
### Non-Functional Requirements
- **NFR-001**: All UI composables and business logic MUST reside in `commonMain` (KMP — Constitution §I, §III).
- **NFR-002**: Float values MUST be pre-formatted with `NumberFormatter.format()` / `MetricFormatter` (CMP constraint).
- **NFR-003**: Metric charts MUST use a minimum x-step of 60 seconds to prevent Vico slot-count explosion with irregular timestamps.
- **NFR-004**: Adaptive layout MUST switch between side-by-side (≥600dp) and stacked (< 600dp) chart/list arrangement.
- **NFR-005**: Chart expand/collapse toggle MUST animate with `AnimatedVisibility`.
## Source-Set Impact
| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | All ~70 files in scope | All business logic, Compose UI, ViewModels, and navigation |
| `androidMain` | Platform `expect` implementations only | `CompassHeadingProvider`, `PhoneLocationProvider`, `MagneticFieldProvider` |
| `jvmMain` | Desktop `expect` implementations | Same three compass providers (stubs) |
## Design Standards Compliance
- [x] New screens reviewed against [design standards](https://raw.githubusercontent.com/meshtastic/design/refs/heads/master/standards/meshtastic_design_standards_latest.md)
- [x] M3 component selection verified (SectionCard, ListItem, SwitchListItem, FilterChip, AssistChip, etc.)
- [x] Accessibility: TalkBack semantics on loading spinner, chart expand/collapse, all icon buttons
- [x] Typography: `titleMediumEmphasized` for card timestamps, M3 scale throughout
## Privacy Assessment
- [x] No PII, location data, or cryptographic keys logged or exposed (public keys displayed to user, never transmitted)
- [x] No new network calls that transmit user data (hardware image fetched from public CDN)
- [x] Proto submodule (`core/proto`) not modified (read-only upstream)
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: All nine metric log screens render without crashes for nodes with 0, 1, and 100+ data points.
- **SC-002**: Time-frame filtering correctly limits displayed data and enables/disables selector chips.
- **SC-003**: Chart↔card selection sync scrolls to the matching item within 300ms.
- **SC-004**: CSV export produces valid, importable files with correct column headers.
- **SC-005**: Traceroute request/response pairing correctly matches by packet ID with accurate hop counts and RTT.
- **SC-006**: Compass shows correct bearing ±1° when phone location and target position are both available.
- **SC-007**: Remote admin session handshake completes within 10 seconds or shows timeout snackbar.
## Assumptions
- All business logic and UI composables reside in `commonMain` source set.
- String resources added to `core/resources/src/commonMain/composeResources/values/strings.xml`.
- Icons use `MeshtasticIcons` (from `core/ui/icon/`).
- Float values pre-formatted with `NumberFormatter.format()` (CMP constraint).
- Vico charting library (Patrykandpatrick) is the standard for all metric graphs.
- Platform providers for compass (`CompassHeadingProvider`, `PhoneLocationProvider`, `MagneticFieldProvider`) have `expect`/`actual` implementations per target.
- `GetNodeDetailsUseCase` aggregates node identity, metrics state, and environment state into a single reactive flow.
- The traceroute map screen is provided via `LocalTracerouteMapScreenProvider` composition local.

View File

@@ -1,340 +0,0 @@
# Tasks: Node Detail & Metrics
**Spec**: [spec.md](spec.md) | **Plan**: [plan.md](plan.md) | **Status**: Migrated
**Prefix**: NDM-T | **Date**: 2025-07-15
> All tasks marked `[x]` reflect existing, shipped implementation.
> Tasks marked `[ ]` are identified **gaps** — code without tests, missing error handling, or areas for improvement.
---
## Phase 1 — Data Layer & Models
### NDM-T001: MetricsState data class ✅
- [x] Create `MetricsState` data class aggregating device, signal, power, host, traceroute, neighbor, position, and pax metrics
- [x] Include `hasXxxMetrics()` convenience methods
- [x] Include `oldestTimestampSeconds()` for time-frame availability
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/MetricsState.kt`
### NDM-T002: TimeFrame enum ✅
- [x] Define `TimeFrame` enum with entries: ONE_HOUR, TWENTY_FOUR_HOURS, SEVEN_DAYS, TWO_WEEKS, ONE_MONTH, ALL_TIME
- [x] Implement `timeThreshold()` and `isAvailable()` methods
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/TimeFrame.kt`
### NDM-T003: LogsType enum ✅
- [x] Define `LogsType` enum with 9 entries mapping to route factories, icons, and title resources
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/LogsType.kt`
### NDM-T003a: NodeDetailAction sealed interface ✅
- [x] Define sealed action types: Navigate, TriggerServiceAction, HandleNodeMenuAction, OpenRemoteAdmin, RefreshMetadata, ShareContact, OpenCompass
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/model/NodeDetailAction.kt`
### NDM-T003b: EnvironmentMetricsState ✅
- [x] Create `EnvironmentMetricsState` with graphing data extraction and Fahrenheit conversion support
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt`
---
## Phase 2 — Node Detail Screen
### NDM-T004: NodeDetailViewModel ✅
- [x] Build ViewModel combining node identity, metrics state, session status, and cooldown timestamps
- [x] Reactive `uiState` flow via `combine` + `flatMapLatest` over active node ID
- [x] `handleNodeMenuAction` for remove, ignore, mute, favorite, request telemetry, traceroute
- [x] `openRemoteAdmin` with session handshake and snackbar feedback
- [x] `refreshMetadata`, `setNodeNotes`, `getDirectMessageRoute`
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt`
### NDM-T005: NodeDetailScreens (Scaffold + Overlays) ✅
- [x] `NodeDetailScreen` composable with LaunchedEffect for nodeId start and navigation events
- [x] `NodeDetailScaffold` with MainAppBar, overlay state management
- [x] `NodeDetailOverlays` for SharedContact dialog, FirmwareRelease bottom sheet, Compass bottom sheet
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreens.kt`
### NDM-T006: NodeDetailContent ✅
- [x] `Crossfade` between loading spinner and `NodeDetailList`
- [x] `NodeDetailList` as LazyColumn with NodeDetailsSection, DeviceActions, DeviceDetailsSection, NotesSection, AdministrationSection
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailContent.kt`
### NDM-T007: NodeDetailsSection (Identity Card) ✅
- [x] Display name, role, node ID, node number, last heard, hops, user ID, uptime
- [x] Signal row (SNR + RSSI) for direct-heard nodes
- [x] MQTT + manual verification row
- [x] Public key display with base64 encoding and copy-on-long-press
- [x] MismatchKeyWarning for encryption key errors
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt`
### NDM-T008: DeviceDetailsSection ✅
- [x] Hardware model image from CDN with Coil3 async loading and fallback
- [x] Hardware display name with optional PlatformIO target
- [x] Support status (officially supported vs community supported)
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DeviceDetailsSection.kt`
### NDM-T009: DeviceActions ✅
- [x] Primary row: Direct Message button, Share Contact button, Favorite toggle
- [x] Management: Ignore switch, Mute switch, Remove action
- [x] `isEffectivelyUnmessageable` check to hide DM button
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DeviceActions.kt`
### NDM-T010: AdministrationSection + FirmwareSection ✅
- [x] Remote admin with session status chip (NoSession, Active, Stale)
- [x] Progress indicator during session handshake
- [x] Refresh metadata button
- [x] Firmware edition, installed version, latest stable, latest alpha with colour-coded version comparison
- [x] Firmware release info bottom sheet
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.kt`
### NDM-T010a: TelemetricActionsSection ✅
- [x] Build 11 telemetric feature rows (user info, traceroute, neighbor info, signal, device, environment, air quality, power, host, pax, position)
- [x] Log navigation buttons with tooltip
- [x] Cooldown-guarded request buttons
- [x] Inline content for environment metrics, power metrics, and position (map + compass)
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/TelemetricActionsSection.kt`
### NDM-T010b: NotesSection ✅
- [x] Editable notes section with save callback
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NotesSection.kt`
---
## Phase 3 — Chart Infrastructure
### NDM-T011: BaseMetricScreen template ✅
- [x] `BaseMetricScreen` generic scaffold with AppBar (export, expand/collapse, info, refresh), controlPart, chartPart, listPart
- [x] Bi-directional chart↔card selection sync via `selectedX` + `animateScrollToItem`/`animateScroll`
- [x] `AdaptiveMetricLayout` with responsive split at 600dp
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt`
### NDM-T012: GenericMetricChart ✅
- [x] Vico `CartesianChartHost` wrapper with multi-layer support, dual axes, markers, FadingEdges, zoom
- [x] `MarkerVisibilityListener` for point selection
- [x] Minimum x-step of 60 seconds to prevent slot-count explosion
- [x] `MetricChartScaffold` with `CartesianChartModelProducer` + Legend
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt`
### NDM-T013: ChartStyling + CommonCharts ✅
- [x] Line style factories: `createBoldLine`, `createSubtleLine`, `createDashedLine`, `createGradientLine`, `createStyledLine`
- [x] Marker value formatter with colour-based label routing
- [x] Threshold line decoration (`rememberThresholdLine`)
- [x] Bottom time axis with `MetricFormatter`
- **Files**: `feature/node/metrics/ChartStyling.kt`, `feature/node/metrics/CommonCharts.kt`
### NDM-T013a: TimeFrameSelector ✅
- [x] Horizontal chip row for time-frame selection with dynamic availability
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TimeFrameSelector.kt`
### NDM-T013b: MetricLogComponents (shared primitives) ✅
- [x] `MetricIndicator`, `MetricValueRow`, `SelectableMetricCard`, `Legend`, `LegendInfoDialog`, `DeleteItem`
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricLogComponents.kt`
---
## Phase 4 — Metric Screens
### NDM-T014: DeviceMetricsScreen ✅
- [x] Dual-axis chart: battery + ch util + air util (left, 0100%), voltage (right)
- [x] 20% battery threshold line
- [x] Dynamic legend filtering based on available data
- [x] `DeviceMetricsCard` with battery info, channel/air util, uptime
- [x] CSV export via `saveDeviceMetricsCSV`
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt`
### NDM-T015: EnvironmentMetricsScreen ✅
- [x] Full environment chart with multi-series environment data
- [x] `EnvironmentMetricsCard` with sub-displays: temperature, humidity, pressure, soil, gas, IAQ, lux, UV, voltage, current, radiation, wind, rainfall, one-wire (up to 8)
- [x] Fahrenheit conversion in `filteredEnvironmentMetrics`
- [x] CSV export via `saveEnvironmentMetricsCSV` with 8 one-wire columns
- **Files**: `feature/node/metrics/EnvironmentMetrics.kt`, `feature/node/metrics/EnvironmentCharts.kt`
### NDM-T016: SignalMetricsScreen ✅
- [x] Dual-axis chart: RSSI (left, solid blue line) + SNR (right, dashed green line)
- [x] `SignalMetricsCard` with RSSI, SNR values and `LoraSignalIndicator`
- [x] CSV export via `saveSignalMetricsCSV`
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.kt`
### NDM-T017: PowerMetricsScreen ✅
- [x] Per-channel chart with `FilterChip` channel selector (up to 8 channels)
- [x] Dual-axis: current (left) + voltage (right)
- [x] `PowerMetricsCard` with per-channel voltage/current rows (up to 3 rows of 3)
- [x] CSV export via `savePowerMetricsCSV`
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.kt`
### NDM-T018: TracerouteLogScreen ✅
- [x] `resolveTraceroutePoints` pairing requests with responses by packet ID
- [x] `TracerouteMetricsChart` with forward hops (blue), return hops (green), RTT (orange)
- [x] `TracerouteCard` with route summary text, hop counts, RTT
- [x] `showTracerouteDetail` dialog with annotated route + "View on Map" button
- [x] Map availability validation before navigation
- **Files**: `feature/node/metrics/TracerouteLog.kt`, `feature/node/metrics/TracerouteChart.kt`
### NDM-T019: PositionLogScreen ✅
- [x] Track map via `LocalNodeTrackMapProvider` with selected position highlighting
- [x] `PositionCard` with coordinates, altitude, speed, heading, satellite count
- [x] Position request + clear buttons
- [x] CSV export via `savePositionCSV`
- **Files**: `feature/node/metrics/PositionLogScreens.kt`, `feature/node/metrics/PositionLogComponents.kt`
### NDM-T020: HostMetricsLogScreen ✅
- [x] `HostMetricsChart` with load averages (1/5/15) and optional free memory series
- [x] `HostMetricsCard` with uptime, free memory, disk free (up to 3 partitions), load averages with coloured progress bars, user_string
- [x] `formatBytes` helper with KB/MB/GB formatting
- **Files**: `feature/node/metrics/HostMetricsLog.kt`, `feature/node/metrics/HostMetricsChart.kt`
### NDM-T021: PaxMetricsScreen ✅
- [x] Three-series chart: total (gray), BLE (purple), WiFi (orange)
- [x] `PaxMetricsItem` with total, BLE, WiFi counts and uptime
- [x] `decodePaxFromLog` with binary proto, Base64, and hex fallback paths
- [x] Empty state message when no pax data
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.kt`
### NDM-T022: NeighborInfoLogScreen ✅
- [x] Request/response pairing by packet ID
- [x] Annotated neighbour info with colour-coded signal quality
- [x] Cooldown-guarded refresh button
- [x] Long-press to delete log entry
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.kt`
---
## Phase 5 — Compass
### NDM-T023: CompassViewModel ✅
- [x] Heading from `CompassHeadingProvider`, location from `PhoneLocationProvider`
- [x] True-north correction via `MagneticFieldProvider.getDeclination`
- [x] Bearing, distance, alignment detection (within 5°)
- [x] Positional accuracy from GPS accuracy × DOP or precision bits
- [x] Angular error calculation
- [x] Warning states: NO_MAGNETOMETER, NO_LOCATION_PERMISSION, LOCATION_DISABLED, NO_LOCATION_FIX
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.kt`
### NDM-T024: CompassBottomSheet ✅
- [x] Compass sheet composable with heading ring, bearing pointer, distance, accuracy
- [x] Request location permission / open location settings callbacks
- [x] Lifecycle-aware start/stop via `DisposableEffect`
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/CompassBottomSheet.kt`
---
## Phase 6 — Navigation
### NDM-T025: NodesNavigation graph ✅
- [x] Nav3 `entry<T>` declarations for `NodesRoute.NodeDetail`, all 9 `NodeDetailRoute.*` screens, and `TracerouteMap`
- [x] `ListDetailSceneStrategy` pane metadata (listPane, detailPane, extraPane)
- [x] `NodeDetailScreen` enum mapping route classes to screen composables
- [x] `MetricsViewModel` scoped per `destNum` with `@InjectedParam`
- **File**: `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/NodesNavigation.kt`
---
## Phase 7 — Testing
### NDM-T026: NodeDetailViewModelTest ✅
- [x] Test initialization
- [x] Test `uiState` emits updates from use case
- [x] Test `handleNodeMenuAction` delegates Mute to `nodeManagementActions`
- [x] Test `handleNodeMenuAction` delegates TraceRoute to `nodeRequestActions`
- **File**: `feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModelTest.kt`
### NDM-T027: MetricsViewModelTest ✅
- [x] Test initialization
- [x] Test `state` reflects updates from `getNodeDetailsUseCase`
- [x] Test `availableTimeFrames` filters based on oldest data
- [x] Test `savePositionCSV` writes correct header and coordinate data
- **File**: `feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModelTest.kt`
### NDM-T028: TracerouteChartTest ✅
- [x] `matchesRequestToResult` — pairs by packet ID
- [x] `computesForwardHops` — 2 intermediate → 2 hops
- [x] `directRoute_yieldsZeroHops` — no intermediates → 0 hops
- [x] `computesRoundTripSeconds` — 3.5s RTT calculation
- [x] `noMatchingResult_yieldsNulls` — mismatched ID → all nulls
- [x] `emptyInputs_returnsEmpty`
- [x] `multipleRequests_preservesOrder`
- [x] `emptyRouteBack_yieldsNullReturnHops`
- [x] `timeSeconds_truncatesSubSecondPrecision`
- [x] `returnHops_computedWhenRouteBackAvailable`
- **File**: `feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/TracerouteChartTest.kt`
### NDM-T029: TimeFrameTest ✅
- [x] `timeThreshold` for all entries
- [x] `isAvailable` with boundary, just-under, and data-range checks
- **File**: `feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/model/TimeFrameTest.kt`
### NDM-T030: DecodePaxFromLogTest ✅
- [x] Binary proto valid decode, want_response filter, all-zero filter, wrong portnum
- [x] Base64 fallback valid decode
- [x] Invalid raw message and empty log return null
- **File**: `feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/DecodePaxFromLogTest.kt`
### NDM-T031: EnvironmentMetricsStateTest ✅
- [x] Graphing data time range extraction
- [x] Zero temperature handled as valid (not filtered)
- **File**: `feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsStateTest.kt`
### NDM-T032: FormatBytesTest ✅
- [x] Zero, small values, KB/MB/GB boundaries, decimals, negative, custom decimal places
- **File**: `feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/FormatBytesTest.kt`
### NDM-T033: CompassViewModelTest ✅
- [x] Compass state updates (heading, bearing, distance)
- **File**: `feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/compass/CompassViewModelTest.kt`
### NDM-T034: HandleNodeActionTest ✅
- [x] Action routing dispatch tests
- **File**: `feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/HandleNodeActionTest.kt`
### NDM-T035: NodeManagementActionsTest ✅
- [x] Favorite, mute, ignore, remove action delegation tests
- **File**: `feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt`
---
## Identified Gaps
### NDM-T100: Missing — MetricsViewModel CSV export tests for device/environment/signal/power
- [x] Add unit tests for `saveDeviceMetricsCSV`, `saveEnvironmentMetricsCSV`, `saveSignalMetricsCSV`, `savePowerMetricsCSV` verifying correct column headers and data formatting
- **Rationale**: Only `savePositionCSV` has a test; the other four export methods are untested.
- **Priority**: Medium
### NDM-T101: Missing — HostMetricsLogScreen chart+card test coverage
- [x] Add unit tests for `HostMetricsChart` data model and `formatBytes` edge cases (exact boundaries)
- **Rationale**: `formatBytes` is tested but chart data transformation and card selection sync are not.
- **Priority**: Low
### NDM-T102: Missing — Compass accuracy edge cases
- [x] Add tests for `calculatePositionalAccuracyMeters` with various DOP combinations (PDOP-only, HDOP+VDOP, HDOP-only, precision-bits-only, and none)
- [x] Add test for `calculateAngularError` when distance is zero
- **Rationale**: `CompassViewModelTest` exists but accuracy calculation branch coverage is not verified.
- **Priority**: Medium
### NDM-T103: Missing — Environment NaN guard tests
- [x] Add tests verifying that `NaN` temperature, humidity, and pressure values are correctly filtered (not rendered, not charted)
- **Rationale**: The code has `isNaN()` guards but no tests validate them.
- **Priority**: Low
### NDM-T104: Missing — Remote admin session timeout testing
- [x] Add `NodeDetailViewModelTest` coverage for `openRemoteAdmin` with `Disconnected` and `Timeout` session results
- **Rationale**: Only `Mute` and `TraceRoute` actions are tested; session error paths are untested.
- **Priority**: Medium
### NDM-T105: Missing — Adaptive layout breakpoint test
- [ ] **[DEFERRED]** Add UI test or screenshot test verifying `AdaptiveMetricLayout` switches from Column to Row at 600dp — *Deferred: requires Compose UI test infrastructure for adaptive layout breakpoints.*
- **Rationale**: Responsive layout is untested.
- **Priority**: Low
---
## Summary
| Category | Total | Complete | Gaps |
|----------|-------|----------|------|
| Data Layer | 5 | 5 | 0 |
| Detail Screen | 8 | 8 | 0 |
| Chart Infrastructure | 5 | 5 | 0 |
| Metric Screens | 9 | 9 | 0 |
| Compass | 2 | 2 | 0 |
| Navigation | 1 | 1 | 0 |
| Testing | 10 | 10 | 0 |
| **Gaps** | 6 | 3 | **3** |
| **Total** | **46** | **43** | **3** |

View File

@@ -1,203 +0,0 @@
# Implementation Plan: Radio & App Settings
**Branch**: `008-radio-app-settings` | **Date**: 2025-07-17 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/008-radio-app-settings/spec.md`
**Status**: Migrated — reverse-engineered from existing `feature/settings` module.
## Summary
The Radio & App Settings feature provides the complete device and application configuration experience across 76 common source files (~12,100 lines). It uses a `RadioConfigViewModel` with async protobuf requestresponse tracking to read/write all radio, device, and module configurations, a `SettingsViewModel` for app-level preferences via DataStore, a `DebugViewModel` for log inspection with search/filter/export, plus supporting ViewModels for channels, filter settings, and node database cleanup. Navigation uses Navigation 3 `settingsGraph` with `entry<>` pattern. All UI is Compose Multiplatform in `commonMain` with platform-specific `expect`/`actual` declarations for 5 screens.
## Technical Context
**Language/Version**: Kotlin 2.3+ targeting JDK 21
**Primary Dependencies**: Compose Multiplatform, Material 3 Adaptive, Koin 4.2+ (K2 Compiler Plugin), Room KMP, DataStore KMP, Wire (protobuf), Turbine (testing), Mokkery (mocking), Kotest (assertions)
**Storage**: DataStore KMP for app preferences (theme, locale, analytics, notifications, mesh log, filter words); Room KMP for mesh logs and node database
**Testing**: KMP `allTests` for `feature:settings` module — 10 test files, ~1,689 lines
**Target Platform**: Android, Desktop (JVM), iOS — all via `commonMain`
**Performance Goals**: 60fps scrolling on all config screens; config response timeout ≤ 30 seconds
**Constraints**: All UI in `commonMain`; no `java.*`/`android.*` in common; CMP float pre-formatting via `NumberFormatter.format()`; Wire protobuf messages use `ConfigState<T>` with `rememberSaveable` for process death survival
**Scale/Scope**: 76 commonMain files, 13 androidMain, 8 jvmMain, 8 iosMain, 1 jvmAndroidMain, 10 commonTest
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Kotlin Multiplatform Core | ✅ PASS | All business logic and ViewModels in `commonMain`. Platform code limited to `expect`/`actual` for 5 screens + utilities. |
| II. Zero Lint Tolerance | ✅ PASS | `spotlessApply` + `detekt` pass. `@Suppress` annotations used for justified violations (`LongParameterList`, `CyclomaticComplexMethod`, `MagicNumber`). |
| III. Compose Multiplatform UI | ✅ PASS | All composables use CMP APIs. `NumberFormatter.format()` used for floats. Navigation 3 `settingsGraph` pattern with `entry<>`. |
| IV. Privacy First | ✅ PASS | Analytics respects opt-out toggle. Location sharing is user-initiated with permission checks. Security config export is user-initiated. No PII logging. |
| V. Design Standards Compliance | ✅ PASS | M3 components: `ListItem`, `SwitchListItem`, `SwitchPreference`, `DropDownPreference`, `ExpressiveSection`, `MainAppBar`. Error colors for admin actions. |
| VI. Verify Before Push | ✅ PASS | Full verification: `./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests`. |
| VII. Coroutine Safety | ✅ PASS | `safeCatching {}` used (not `runCatching {}`). `safeLaunch(tag = ...)` for coroutine scope. Project `ioDispatcher` via `CoroutineDispatchers`. |
| VIII. Resource Discipline | ✅ PASS | All strings via `stringResource(Res.string.key)`. Icons via `MeshtasticIcons`. |
| IX. Branch & Scope Hygiene | ✅ PASS | Brownfield migration — feature is stable on main. |
**Gate Result**: ✅ All principles satisfied
## Project Structure
### Documentation (this feature)
```text
specs/008-radio-app-settings/
├── spec.md # Feature specification (migrated)
├── plan.md # This file (migrated)
└── tasks.md # Task breakdown (migrated)
```
### Source Code (repository root)
```text
feature/settings/
├── src/commonMain/kotlin/org/meshtastic/feature/settings/
│ ├── di/
│ │ └── FeatureSettingsModule.kt ← Koin DI module (@ComponentScan)
│ ├── navigation/
│ │ ├── SettingsNavigation.kt ← Nav 3 settingsGraph with entry<>
│ │ ├── SettingsNavUtils.kt ← Shared navigation helpers
│ │ ├── ConfigRoute.kt ← 10 device config routes enum
│ │ ├── ModuleRoute.kt ← 16 module config routes enum
│ │ └── AboutLibrariesLoader.kt ← expect/actual for OSS licenses
│ ├── radio/
│ │ ├── RadioConfigViewModel.kt ← Core config VM (769 lines)
│ │ ├── RadioConfig.kt ← Radio config list, AdminRoute enum
│ │ ├── RadioConfigState.kt (inline) ← Immutable config state data class
│ │ ├── ResponseState.kt ← Generic sealed class: Empty/Loading/Success/Error
│ │ ├── CleanNodeDatabaseViewModel.kt ← Node DB cleanup VM
│ │ ├── CleanNodeDatabaseScreen.kt ← Node DB cleanup UI
│ │ ├── channel/
│ │ │ ├── ChannelConfigScreen.kt ← Channel editor screen
│ │ │ ├── ChannelScreen.kt ← Channel display
│ │ │ ├── ChannelsNavigation.kt ← Channel sub-navigation
│ │ │ └── component/ ← ChannelCard, EditChannelDialog, etc.
│ │ └── component/ ← 28 config screens (one per config type)
│ │ ├── ConfigState.kt ← Generic config state holder with Saver
│ │ ├── DeviceConfigScreen.kt ← Device config (expect/actual)
│ │ ├── SecurityConfigScreen.kt ← Security config (expect/actual)
│ │ ├── PositionConfigScreen.kt ← Position config (expect/actual)
│ │ ├── ExternalNotificationConfigScreen.kt ← ExtNotif (expect/actual)
│ │ ├── LoRaConfigItemList.kt ← LoRa settings
│ │ ├── MQTTConfigItemList.kt ← MQTT settings with probe
│ │ ├── UserConfigItemList.kt ← User identity settings
│ │ ├── BluetoothConfigItemList.kt ← Bluetooth settings
│ │ └── ... (15 more module config screens)
│ ├── channel/
│ │ └── ChannelViewModel.kt ← Channel URL parsing, channel set mgmt
│ ├── debugging/
│ │ ├── Debug.kt ← Debug screen composables (460 lines)
│ │ ├── DebugViewModel.kt ← Log display/search/filter/export VM
│ │ ├── DebugSearch.kt ← Search bar + filter bar composables
│ │ ├── DebugFilters.kt ← Filter logic composables
│ │ ├── LogExporter.kt ← expect/actual platform log export
│ │ └── LogFormatter.kt ← Log message formatting
│ ├── filter/
│ │ ├── FilterSettingsScreen.kt ← Message word filter UI
│ │ └── FilterSettingsViewModel.kt ← Filter preferences VM
│ ├── component/
│ │ ├── PrivacySection.kt ← Analytics + location toggles
│ │ ├── NotificationSection.kt ← Notification toggles
│ │ ├── ExpressiveSection.kt ← Reusable M3 section container
│ │ ├── HomoglyphSetting.kt ← Homoglyph encoding toggle
│ │ └── ThemePickerDialog.kt ← Theme selection dialog
│ ├── tak/
│ │ ├── TakPermissionUtil.kt ← expect/actual TAK permissions
│ │ └── PrefExporter.kt ← expect/actual XML pref export
│ ├── util/
│ │ ├── SettingsIntervals.kt ← Shared interval constants
│ │ ├── FixedUpdateIntervals.kt ← Fixed telemetry intervals
│ │ └── Formatting.kt ← Value formatting helpers
│ ├── SettingsViewModel.kt ← App preferences VM (195 lines)
│ ├── DeviceConfigurationScreen.kt ← Device config list screen
│ ├── ModuleConfigurationScreen.kt ← Module config list screen
│ ├── AdministrationScreen.kt ← Admin actions screen
│ └── AboutScreen.kt ← OSS acknowledgements screen
├── src/androidMain/ ← 13 platform-specific files
├── src/jvmMain/ ← 8 platform-specific files
├── src/iosMain/ ← 8 platform-specific files
├── src/jvmAndroidMain/ ← 1 shared JVM/Android file
└── src/commonTest/ ← 10 test files (~1,689 lines)
├── radio/RadioConfigViewModelTest.kt ← 535 lines, 13 tests
├── radio/component/EditDeviceProfileDialogTest.kt
├── radio/component/MapReportingPreferenceTest.kt
├── radio/CleanNodeDatabaseViewModelTest.kt
├── SettingsViewModelTest.kt ← 264 lines, 13 tests
├── debugging/DebugViewModelTest.kt ← 189 lines, 6 tests
├── debugging/DebugSearchTest.kt ← 188 lines, 5 compose UI tests
├── debugging/LogFormatterTest.kt
├── channel/CommonChannelViewModelTest.kt ← 103 lines, 4 tests
└── filter/FilterSettingsViewModelTest.kt ← 72 lines, 3 tests
```
**Structure Decision**: The feature is organized by functional area (radio, debugging, filter, channel, component) within a single `feature/settings` module. This is appropriate given all areas share the `RadioConfigViewModel` and navigation graph. Platform-specific code is isolated to `expect`/`actual` declarations.
## Module Impact
| Module | Change Type | Files Affected | Risk |
|--------|-------------|----------------|------|
| `feature/settings` | Existing | 76 commonMain + 30 platform | Low (stable) |
| `core/domain` | Dependency | ~15 use cases | Low (consumed only) |
| `core/repository` | Dependency | ~12 repositories/prefs | Low (consumed only) |
| `core/navigation` | Dependency | `SettingsRoute` enum | Low (route definitions) |
| `core/ui` | Dependency | `ListItem`, `SwitchListItem`, `MainAppBar`, `MeshtasticIcons` | Low (shared components) |
| `core/resources` | Dependency | strings.xml, drawables | Low (resource references) |
## Integration Points
- **Navigation**: `SettingsRoute` sealed class in `core/navigation` defines all route keys. `settingsGraph()` registers `entry<>` providers for each route.
- **DI**: `FeatureSettingsModule` uses `@ComponentScan` for automatic registration of `@KoinViewModel` classes.
- **DataStore**: App preferences flow through repository interfaces (`UiPrefs`, `MeshLogPrefs`, `NotificationPrefs`, `FilterPrefs`, `MapConsentPrefs`, `AnalyticsPrefs`, `HomoglyphPrefs`).
- **RadioController**: Admin messages and config writes go through `RadioConfigUseCase``RadioController`.
- **ServiceRepository**: `meshPacketFlow` provides real-time response packets for `processRadioResponseUseCase`.
- **MqttManager**: MQTT probe connects directly to broker for reachability/credential testing.
## Design Constraints
- All UI lives in `commonMain` — not platform-specific
- Strings accessed via `stringResource(Res.string.key)` — never hardcoded
- Icons use `MeshtasticIcons` exclusively (from `core/ui/icon/`)
- Error handling uses `safeCatching {}` not `runCatching {}`
- Dispatchers via `org.meshtastic.core.common.util.ioDispatcher`
- Float values must be pre-formatted with `NumberFormatter.format()` (CMP constraint)
- Wire protobuf messages wrapped in `ConfigState<T>` with `rememberSaveable` Saver for process death
- Config request timeout is 30 seconds, enforced by `registerRequestId` coroutine
## Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| Config response race condition (multiple rapid navigations) | Low | Medium | `clearPacketResponse()` on screen exit; `requestIds` set tracking |
| Remote admin timeout on slow mesh | Medium | Low | 30-second timeout with user-visible error and retry |
| Platform `expect`/`actual` drift across Android/Desktop/iOS | Low | Medium | Shared `commonMain` logic; platform code is thin wrappers |
| Module route bitfield changes in firmware | Low | Low | `Capabilities` version-gating; `isSupported` lambda per route |
## Phase Alignment with Tasks
| Phase | Purpose | Key Tasks | Dependencies |
|-------|---------|-----------|--------------|
| 1. Foundation | DI, navigation, state models | SET-T001SET-T005 | None |
| 2. Radio Config | User, LoRa, Channels, Security config | SET-T006SET-T015 | Phase 1 |
| 3. Device Config | Device, Position, Power, Network, Display, Bluetooth | SET-T016SET-T022 | Phase 1 |
| 4. Module Config | 16 module config screens | SET-T023SET-T030 | Phase 1 |
| 5. App Preferences | Theme, locale, analytics, notifications, persistence | SET-T031SET-T038 | Phase 1 |
| 6. Administration & Advanced | Admin actions, profile backup, node DB cleanup | SET-T039SET-T045 | Phase 2 |
| 7. Debug Panel | Log display, search, filter, export | SET-T046SET-T051 | Phase 1 |
| 8. Testing | ViewModel tests, compose UI tests | SET-T052SET-T061 | All prior |
### Critical Path
```
Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5 → Phase 6 → Phase 7 → Phase 8
(all phases feed testing)
```
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| *None* | — | — |

View File

@@ -1,296 +0,0 @@
# Feature Specification: Radio & App Settings
**Feature Branch**: `008-radio-app-settings`
**Created**: 2025-07-17
**Status**: Migrated
**Input**: Brownfield migration of existing `feature/settings` module — radio config, device config, module config, channel config, app preferences, notification settings, debug panel, administration, filter settings, and About screen.
## Summary
The Radio & App Settings feature is the largest module in the Meshtastic app (~76 common source files, ~12,100 lines), providing the complete device and application configuration experience. It encompasses radio configuration (User, LoRa, Channels, Security), device configuration (Device, Position, Power, Network, Display, Bluetooth), module configuration (16 modules from MQTT to TAK), app preferences (theme, locale, analytics, location sharing, notifications, message filtering, homoglyph encoding), device administration (reboot, shutdown, factory reset, node DB reset), profile backup/restore (import/export DeviceProfile protobuf), a debug panel with log inspection/search/filter/export, node database cleanup, and an open-source acknowledgements screen.
## Goals
1. **G-001**: Provide a unified settings hub for all radio, device, module, and app configuration — accessible both locally and via remote administration.
2. **G-002**: Support reading and writing all protobuf-defined radio/module configurations through an async requestresponse pattern with progress tracking and timeout handling.
3. **G-003**: Deliver comprehensive app preference management (theme, locale, analytics, location, notifications, message filtering, database cache, mesh log retention).
4. **G-004**: Enable device administration actions (reboot, shutdown, factory reset, node DB reset) with confirmation dialogs and metadata-aware guards.
5. **G-005**: Provide a full-featured debug panel with log search, multi-filter (AND/OR), decoded protobuf payload inspection, log export, and retention management.
## Non-Goals
- **NG-001**: Node list layout, sorting, and density settings (covered by spec 002).
- **NG-002**: Node detail screens, metrics charts, and compass (covered by spec 007).
- **NG-003**: Firmware OTA update flow (covered by spec 006).
- **NG-004**: Messaging UI and direct message routing (covered by spec 004).
- **NG-005**: Platform-specific system notification channel management (handled by Android OS settings).
## User Scenarios & Testing *(mandatory)*
### User Story 1 — Configure Radio Settings (Priority: P1)
A user navigates to the settings screen to configure core radio parameters: user identity (long name, short name), LoRa region and modem preset, channel configuration with PSK, and security keys — either locally or on a remote node.
**Why this priority**: Radio configuration is the most critical settings function; a device cannot join a mesh without correct LoRa region, channel, and user identity.
**Independent Test**: Connect to a device, navigate to Radio Configuration, edit User config, save, and verify the device receives the admin message.
**Acceptance Scenarios**:
1. **Given** a connected device, **When** the user opens User config, **Then** the current owner info (long name, short name, hardware model) is fetched from the device and displayed in editable fields.
2. **Given** a connected device, **When** the user modifies LoRa config (region, modem preset, tx power) and saves, **Then** a `Config` protobuf is sent via `radioConfigUseCase.setConfig()` and the response state transitions Loading → Success.
3. **Given** a connected device, **When** the user opens Channel Config, **Then** all channels (up to `maxChannels`) are fetched sequentially and displayed with name, PSK, and role.
4. **Given** a remote node with `destNum ≠ myNodeNum`, **When** the user opens any config screen, **Then** the subtitle shows "Remotely administrating {node name}" and all read/write operations target the remote node.
5. **Given** a managed device (`is_managed == true`), **When** the user opens radio config, **Then** a "Device is managed" warning is displayed and config controls are disabled.
---
### User Story 2 — Configure Device Settings (Priority: P1)
A user configures device-level settings: device role, position (including fixed position), power management, network (WiFi/Ethernet), display, and Bluetooth — filtered by device hardware capabilities.
**Why this priority**: Device settings control physical behavior (power mode, display config) that directly affect battery life and usability.
**Independent Test**: Navigate to Device Configuration, verify that only applicable config routes are shown (e.g., Bluetooth hidden for devices without it), edit a config, and confirm the change is sent.
**Acceptance Scenarios**:
1. **Given** a device without Bluetooth metadata (`hasBluetooth == false`), **When** Device Configuration screen renders, **Then** the Bluetooth config route is excluded from the list.
2. **Given** a device without WiFi or Ethernet, **When** Device Configuration screen renders, **Then** the Network config route is excluded.
3. **Given** an open Position config, **When** the user sets a fixed position with coordinates, **Then** `radioConfigUseCase.setFixedPosition()` is called with the position.
4. **Given** an open Network config, **When** the screen loads, **Then** it additionally fetches `DeviceConnectionStatus` to display WiFi/Ethernet connection state.
---
### User Story 3 — Configure Module Settings (Priority: P2)
A user enables and configures one or more of the 16 supported modules (MQTT, Serial, External Notification, Store & Forward, Range Test, Telemetry, Canned Message, Audio, Remote Hardware, Neighbor Info, Ambient Lighting, Detection Sensor, Paxcounter, Status Message, Traffic Management, TAK).
**Why this priority**: Module configuration extends device functionality but is not required for basic mesh operation.
**Independent Test**: Navigate to Module Configuration, open MQTT config, toggle `enabled`, save, and verify the `ModuleConfig` protobuf is sent.
**Acceptance Scenarios**:
1. **Given** a device with `excluded_modules` bitfield set, **When** Module Configuration renders, **Then** excluded modules are hidden from the list.
2. **Given** the user has unlocked excluded modules, **When** Module Configuration renders, **Then** all 16 module routes are shown regardless of bitfield.
3. **Given** a device with role `TAK`, **When** filtering modules, **Then** the TAK module route is visible; for other roles, it is hidden.
4. **Given** the Canned Message module screen, **When** it loads, **Then** `getCannedMessages()` is called and the current messages are displayed for editing.
5. **Given** the External Notification module screen, **When** it loads, **Then** `getRingtone()` is called and the current ringtone is displayed for editing.
---
### User Story 4 — Manage App Preferences (Priority: P2)
A user customizes app-level preferences: theme (light/dark/system), locale, analytics opt-in/out, provide-location-to-mesh toggle, homoglyph encoding, notification settings (messages, node events, low battery), database cache limit, and mesh log retention.
**Why this priority**: App preferences personalize the experience but do not affect mesh operation.
**Independent Test**: Toggle each preference switch and verify the underlying `DataStore` preference is updated via the corresponding use case.
**Acceptance Scenarios**:
1. **Given** the Privacy section, **When** the user toggles "Provide location to mesh", **Then** if granted, `meshLocationUseCase.startProvidingLocation()` is called; if GPS is disabled, a toast is shown.
2. **Given** the Notification section, **When** the user toggles "Messages notifications" off, **Then** `notificationPrefs.messagesEnabled` becomes `false`.
3. **Given** the Persistence section, **When** the user adjusts the database cache slider, **Then** `setDatabaseCacheLimitUseCase` is called with the value clamped to `DatabaseConstants` bounds.
4. **Given** the Persistence section, **When** the user adjusts mesh log retention days, **Then** the value is clamped to `[MIN_RETENTION_DAYS, MAX_RETENTION_DAYS]` and logs older than the threshold are deleted.
---
### User Story 5 — Device Administration (Priority: P2)
A user performs administrative actions on a device: reboot, shutdown, factory reset, or node DB reset, with appropriate confirmation dialogs and metadata-aware guards.
**Why this priority**: Admin actions are destructive/disruptive and need safety guards, but they are essential for device management.
**Independent Test**: Navigate to Administration, trigger each action, verify the confirmation dialog appears, confirm, and verify the admin message is sent.
**Acceptance Scenarios**:
1. **Given** a connected device, **When** the user selects "Reboot" and confirms, **Then** `adminActionsUseCase.reboot(destNum)` is called.
2. **Given** a device where `metadata.canShutdown == false`, **When** the user selects "Shutdown", **Then** an error "Can't shutdown" is displayed instead of sending the command.
3. **Given** a "Node DB Reset" dialog, **When** the user toggles "Preserve favorites" and confirms, **Then** `adminActionsUseCase.nodedbReset()` is called with `preserveFavorites = true`.
4. **Given** a factory reset on the local device (`destNum == myNodeNum`), **When** confirmed, **Then** `factoryReset` is called with `isLocal = true` to additionally clear local state.
---
### User Story 6 — Profile Backup & Restore (Priority: P3)
A user exports a device profile to a file or imports a previously saved profile, applying it to the connected device.
**Why this priority**: Profile management is a power-user feature for device fleet management.
**Independent Test**: Export a profile, verify the file is written; import it back, verify the `DeviceProfile` protobuf is parsed and `installProfileUseCase` is called.
**Acceptance Scenarios**:
1. **Given** a connected local device, **When** the user taps Export, **Then** `exportProfileUseCase` writes a `DeviceProfile` protobuf to the selected URI.
2. **Given** a valid profile file, **When** the user taps Import and selects the file, **Then** `importProfileUseCase` parses it and presents the profile for confirmation before installing.
3. **Given** an invalid profile file, **When** import fails, **Then** the error is propagated and no profile is installed.
---
### User Story 7 — Debug Panel (Priority: P3)
A user inspects mesh packet logs with decoded protobuf payloads, multi-term search with match navigation, multi-tag filtering (AND/OR mode), log retention management, and CSV log export.
**Why this priority**: Debugging is essential for developers and power users but not required for normal app operation.
**Independent Test**: Navigate to Debug Panel, verify logs are displayed with decoded payloads, enter a search term, verify matches are highlighted and navigable.
**Acceptance Scenarios**:
1. **Given** mesh logs exist, **When** the Debug Panel opens, **Then** all logs are displayed as `UiMeshLog` entries with formatted dates, annotated node IDs (hex), and decoded protobuf payloads.
2. **Given** a search term is entered, **When** matches are found, **Then** the user can navigate forward/backward through matches across message, type, date, and payload fields.
3. **Given** multiple filter tags are active in AND mode, **When** filtering, **Then** only logs matching ALL tags are displayed.
4. **Given** the user taps "Clear logs" and confirms, **Then** `meshLogRepository.deleteAll()` is called.
5. **Given** log retention is set to 7 days, **When** saved, **Then** logs older than 7 days are deleted and the preference is persisted.
---
### User Story 8 — Message Filter Settings (Priority: P3)
A user manages a word filter that hides messages containing specific terms.
**Why this priority**: Content filtering is a niche feature for community operators.
**Independent Test**: Add a filter word, verify it appears in the list and the filter pattern is rebuilt.
**Acceptance Scenarios**:
1. **Given** the filter settings screen, **When** the user adds "spam", **Then** `filterPrefs.setFilterWords()` is called with the new set and `messageFilter.rebuildPatterns()` is triggered.
2. **Given** existing filter words, **When** the user removes one, **Then** the word is removed from prefs and patterns are rebuilt.
3. **Given** the filter toggle is off, **When** the user disables filtering, **Then** `filterPrefs.setFilterEnabled(false)` is called.
---
### User Story 9 — Clean Node Database (Priority: P3)
A user removes stale nodes from the local database based on configurable criteria (age threshold, unknown-only filter).
**Why this priority**: Database hygiene is a maintenance feature for long-running nodes.
**Independent Test**: Set "older than 30 days" and "unknown nodes only", preview the node list, confirm deletion.
**Acceptance Scenarios**:
1. **Given** the clean node database screen, **When** the user adjusts the slider and taps preview, **Then** `cleanNodeDatabaseUseCase.getNodesToClean()` returns matching nodes.
2. **Given** a preview list, **When** the user confirms cleaning, **Then** `cleanNodeDatabaseUseCase.cleanNodes()` deletes the listed node nums and the list is cleared.
---
### Edge Cases
- What happens when a config request times out (30-second deadline)? → `ResponseState.Error` with "Timeout" message.
- How does the system handle a shutdown command on hardware that doesn't support it? → Error message "Can't shutdown" based on `metadata.canShutdown`.
- What happens when the user edits channels and the PSK changes? → `packetRepository.migrateChannelsByPSK()` migrates existing messages to the new PSK.
- What happens when `destNum` changes mid-configuration? → `RadioConfigViewModel` re-initializes via `destNumFlow` + `combine`.
- What happens if MQTT probe fails with an exception? → Caught via `safeCatching`, result mapped to `MqttProbeStatus.Other(message)`.
## Architecture
### Key Components
| Component | Module / File | Purpose |
|-----------|---------------|---------|
| `SettingsViewModel` | `feature/settings/SettingsViewModel.kt` | App-level preferences: theme, locale, location, notifications, DB cache, mesh log, data export |
| `RadioConfigViewModel` | `feature/settings/radio/RadioConfigViewModel.kt` | Radio/device/module config read/write, admin actions, profile import/export, MQTT probe |
| `ChannelViewModel` | `feature/settings/channel/ChannelViewModel.kt` | Channel URL parsing, channel set management, LoRa region/TX config |
| `DebugViewModel` | `feature/settings/debugging/DebugViewModel.kt` | Mesh log display, search, filter, export, retention, protobuf payload decoding |
| `FilterSettingsViewModel` | `feature/settings/filter/FilterSettingsViewModel.kt` | Message word filter management |
| `CleanNodeDatabaseViewModel` | `feature/settings/radio/CleanNodeDatabaseViewModel.kt` | Node database cleanup with age/unknown filters |
| `RadioConfigState` | `feature/settings/radio/RadioConfigViewModel.kt` | Immutable state aggregating all config, channels, metadata, connection, response state |
| `ResponseState<T>` | `feature/settings/radio/ResponseState.kt` | Generic sealed class: Empty → Loading(progress) → Success / Error |
| `ConfigRoute` | `feature/settings/navigation/ConfigRoute.kt` | Enum of 10 device config routes with icons, titles, admin message types |
| `ModuleRoute` | `feature/settings/navigation/ModuleRoute.kt` | Enum of 16 module routes with capability/role filtering and excludability bitfield |
| `AdminRoute` | `feature/settings/radio/RadioConfig.kt` | Enum of admin actions: Reboot, Shutdown, Factory Reset, Node DB Reset |
| `SettingsNavigation` | `feature/settings/navigation/SettingsNavigation.kt` | Navigation 3 `settingsGraph` with `entry<>` for all settings routes |
| `FeatureSettingsModule` | `feature/settings/di/FeatureSettingsModule.kt` | Koin DI module with `@ComponentScan` |
| `PrivacySection` | `feature/settings/component/PrivacySection.kt` | Analytics, location sharing, homoglyph encoding toggles |
| `NotificationSection` | `feature/settings/component/NotificationSection.kt` | Messages, node events, low battery notification toggles |
| `ExpressiveSection` | `feature/settings/component/ExpressiveSection.kt` | Reusable M3 section container with title styling |
| `LogSearchManager` | `feature/settings/debugging/DebugViewModel.kt` | Multi-term regex search with match navigation |
| `LogFilterManager` | `feature/settings/debugging/DebugViewModel.kt` | Multi-tag AND/OR log filtering |
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST support reading and writing all protobuf `Config` and `ModuleConfig` types via admin messages with request-response tracking.
- **FR-002**: System MUST display loading progress (completed/total) during multi-packet config reads (e.g., channel enumeration).
- **FR-003**: System MUST timeout config requests after 30 seconds and display an error state.
- **FR-004**: System MUST support local and remote device administration (reboot, shutdown, factory reset, node DB reset) with confirmation dialogs.
- **FR-005**: System MUST guard shutdown actions against hardware that does not support shutdown (`metadata.canShutdown`).
- **FR-006**: System MUST filter device config routes based on hardware capabilities (Bluetooth, WiFi, Ethernet).
- **FR-007**: System MUST filter module routes based on `excluded_modules` bitfield, firmware capability checks, and device role applicability.
- **FR-008**: System MUST support importing and exporting `DeviceProfile` protobuf files.
- **FR-009**: System MUST support exporting security configuration separately.
- **FR-010**: System MUST provide app preference toggles for theme, locale, analytics, location sharing, homoglyph encoding.
- **FR-011**: System MUST provide notification preference toggles for messages, node events, and low battery.
- **FR-012**: System MUST support adjustable database cache limit and mesh log retention with bounded clamping.
- **FR-013**: System MUST provide a debug panel with searchable, filterable, decoded mesh packet logs.
- **FR-014**: System MUST decode mesh packet payloads for known portnums (Position, Telemetry, Routing, AdminMessage, etc.) into human-readable strings.
- **FR-015**: System MUST support message word filtering with add/remove and pattern rebuild.
- **FR-016**: System MUST support node database cleanup by age threshold and unknown-node filter.
- **FR-017**: System MUST migrate channel messages by PSK when channels are updated locally.
- **FR-018**: System MUST support MQTT broker connection probing with reachability/credentials feedback.
- **FR-019**: System MUST display managed device warnings and disable configuration when `is_managed == true`.
- **FR-020**: System MUST support CSV data export of persisted packet data with optional portnum filtering.
### Non-Functional Requirements
- **NFR-001**: All config screens must render at 60fps with smooth scrolling.
- **NFR-002**: Config response timeout must not exceed 30 seconds.
- **NFR-003**: All UI composables reside in `commonMain` — platform-specific code limited to `expect`/`actual` declarations for `SettingsMainScreen`, `LogExporter`, `TakPermissionUtil`, and 4 config screens (Device, ExternalNotification, Position, Security).
- **NFR-004**: All strings accessed via `stringResource(Res.string.key)` — no hardcoded text.
- **NFR-005**: All icons use `MeshtasticIcons` from `core/ui/icon/`.
## Source-Set Impact
| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | 76 source files (~12,100 lines) | All business logic, ViewModels, navigation, and UI composables |
| `androidMain` | 13 files | Platform-specific `SettingsScreen`, `AppInfoSection`, `AppearanceSection`, `PersistenceSection`, `SettingsMainScreen`, `LogExporter`, `LanguageUtils`, `TakPermissionUtil`, `PrefExporter`, and 4 config screen actuals |
| `jvmMain` | 8 files | Desktop `SettingsMainScreen`, `DesktopSettingsScreen`, `LogExporter`, `TakPermissionUtil`, `PrefExporter`, and 4 config screen actuals |
| `iosMain` | 8 files | iOS `SettingsNavigation`, `AboutLibrariesLoader`, `NoopStubs`, `TakPermissionUtil`, `PrefExporter`, and 4 config screen actuals |
| `jvmAndroidMain` | 1 file | Shared `AboutLibrariesLoader` |
| `commonTest` | 10 test files (~1,689 lines) | ViewModel and logic tests |
## Design Standards Compliance
- [x] New screens reviewed against [design standards](https://raw.githubusercontent.com/meshtastic/design/refs/heads/master/standards/meshtastic_design_standards_latest.md)
- [x] M3 component selection verified — `SwitchListItem`, `ListItem`, `ExpressiveSection`, `MainAppBar`, `FilterChip`
- [x] Accessibility: Touch targets via `ListItem`, error colors in Administration section
- [x] Typography: M3 scale via `MaterialTheme.colorScheme` and `MaterialTheme.typography`
## Privacy Assessment
- [x] No PII, location data, or cryptographic keys logged or exposed
- [x] Analytics toggle respects user opt-out (`analyticsPrefs.analyticsAllowed`)
- [x] Location sharing is user-initiated with permission checks and GPS-disabled guards
- [x] Proto submodule (`core/proto`) not modified (read-only upstream)
- [x] Security config export is user-initiated write-to-file only
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: All 10 radio/device config routes and 16 module config routes render and accept user input when connected.
- **SC-002**: Config read/write round-trip completes within 30 seconds or displays a timeout error.
- **SC-003**: All app preference changes are persisted via DataStore and survive app restart.
- **SC-004**: Debug panel displays logs with decoded protobuf payloads for all known portnums.
- **SC-005**: Admin actions (reboot, shutdown, factory reset, node DB reset) execute successfully with confirmation guards.
- **SC-006**: Profile import/export round-trips a `DeviceProfile` protobuf without data loss.
- **SC-007**: ≥10 ViewModel unit test files pass in `commonTest` with full coverage of preference management, connection state, filter logic, and debug search.
## Assumptions
- All business logic and UI composables reside in `commonMain` source set
- String resources added to `core/resources/src/commonMain/composeResources/values/strings.xml`
- Icons use `MeshtasticIcons` (from `core/ui/icon/`)
- Float values pre-formatted with `NumberFormatter.format()` (CMP constraint)
- `SettingsMainScreen` uses `expect`/`actual` because Android uses an `OutlinedCard`-based layout while Desktop uses a different layout
- Remote administration uses the same `RadioConfigViewModel` with `destNum` targeting the remote node
- All protobuf types come from `core/proto` (read-only upstream submodule)
- Koin DI with `@KoinViewModel` and `@ComponentScan` for automatic registration

View File

@@ -1,364 +0,0 @@
# Tasks: Radio & App Settings
**Branch**: `008-radio-app-settings` | **Spec**: [spec.md](spec.md) | **Plan**: [plan.md](plan.md)
**Status**: Migrated — all implemented tasks marked `[x]`. Gap tasks marked `[ ]`.
---
## Phase 1 — Foundation (DI, Navigation, State Models)
- [x] **SET-T001**: Create `FeatureSettingsModule` with `@ComponentScan` for Koin DI auto-registration
- File: `di/FeatureSettingsModule.kt`
- Validates: FR-001
- [x] **SET-T002**: Define `ConfigRoute` enum with 10 device config routes (User, Channels, Device, Position, Power, Network, Display, LoRa, Bluetooth, Security) with icons, titles, and admin message type mappings
- File: `navigation/ConfigRoute.kt`
- Validates: FR-006
- [x] **SET-T003**: Define `ModuleRoute` enum with 16 module routes including `excluded_modules` bitfield filtering, `isSupported` capability checks, and `isApplicable` role filtering
- File: `navigation/ModuleRoute.kt`
- Validates: FR-007
- [x] **SET-T004**: Implement `settingsGraph()` Navigation 3 entry provider registering all settings routes with `entry<>` pattern and shared `RadioConfigViewModel` scoping via `getRadioConfigViewModel()`
- File: `navigation/SettingsNavigation.kt`
- Validates: FR-001
- [x] **SET-T005**: Implement `ResponseState<T>` sealed class (Empty, Loading with progress tracking, Success, Error with UiText) and `RadioConfigState` data class aggregating all config, metadata, channels, and response state
- Files: `radio/ResponseState.kt`, `radio/RadioConfigViewModel.kt` (data class)
- Validates: FR-002, FR-003
---
## Phase 2 — Radio Configuration
- [x] **SET-T006**: Implement `RadioConfigViewModel` core init: `destNumFlow` + `nodeDBbyNum` combine for node resolution, flow collectors for `localConfigFlow`, `channelSetFlow`, `moduleConfigFlow`, `deviceUIConfig`, `fileManifest`, connection state, and `deviceProfileFlow`
- File: `radio/RadioConfigViewModel.kt`
- Validates: FR-001
- [x] **SET-T007**: Implement `setResponseStateLoading()` dispatch: route-specific admin message requests for all `ConfigRoute`, `ModuleRoute`, and `AdminRoute` entries with request ID registration and timeout handling
- File: `radio/RadioConfigViewModel.kt`
- Validates: FR-001, FR-002, FR-003
- [x] **SET-T008**: Implement `processPacketResponse()` handler: dispatch `RadioResponseResult` variants (Metadata, ChannelResponse, Owner, ConfigResponse, ModuleConfigResponse, CannedMessages, Ringtone, ConnectionStatus, Success, Error) and sequential channel fetching
- File: `radio/RadioConfigViewModel.kt`
- Validates: FR-001, FR-002
- [x] **SET-T009**: Implement `setOwner()`, `setConfig()`, `setModuleConfig()` write operations with optimistic state update and request-response tracking
- File: `radio/RadioConfigViewModel.kt`
- Validates: FR-001
- [x] **SET-T010**: Implement `UserConfigItemList` composable for user identity editing (long name, short name, licensed operator)
- File: `radio/component/UserConfigItemList.kt`
- Validates: FR-001, US-1
- [x] **SET-T011**: Implement `LoRaConfigItemList` composable for LoRa region, modem preset, bandwidth, hop limit, tx power, PA fan control
- File: `radio/component/LoRaConfigItemList.kt`
- Validates: FR-001, US-1
- [x] **SET-T012**: Implement `SecurityConfigScreen` (expect/actual) for admin key, encryption keys, managed device flag
- File: `radio/component/SecurityConfigScreen.kt`
- Validates: FR-001, FR-009, FR-019
- [x] **SET-T013**: Implement channel configuration system: `ChannelConfigScreen`, `ChannelScreen`, `ChannelCard`, `EditChannelDialog`, `ChannelConfigHeader`, `ChannelLegend`, sequential channel fetch and `updateChannels()` with PSK migration
- Files: `radio/channel/*.kt`, `radio/channel/component/*.kt`
- Validates: FR-001, FR-017, US-1
- [x] **SET-T014**: Implement `ChannelViewModel` for channel URL parsing, channel set management, LoRa region/TX config, share tracking
- File: `channel/ChannelViewModel.kt`
- Validates: FR-001, US-1
- [x] **SET-T015**: Implement `ConfigState<T>` generic state holder with Wire `Message.Adapter` Saver for `rememberSaveable` process death survival, and `rememberConfigState()` composable
- File: `radio/component/ConfigState.kt`
- Validates: NFR-001
---
## Phase 3 — Device Configuration
- [x] **SET-T016**: Implement `DeviceConfigurationScreen` with hardware-filtered config route list (`filterExcludedFrom` for Bluetooth, WiFi, Ethernet metadata)
- File: `DeviceConfigurationScreen.kt`
- Validates: FR-006, US-2
- [x] **SET-T017**: Implement `DeviceConfigScreen` (expect/actual) for device role, rebroadcast mode, serial debug, node info broadcast interval
- File: `radio/component/DeviceConfigScreen.kt`
- Validates: FR-001, US-2
- [x] **SET-T018**: Implement `PositionConfigScreen` (expect/actual) for GPS, fixed position set/remove, position broadcast, smart position
- File: `radio/component/PositionConfigScreen.kt`
- Validates: FR-001, US-2
- [x] **SET-T019**: Implement `PowerConfigScreen` for power management settings
- File: `radio/component/PowerConfigItemList.kt`
- Validates: FR-001, US-2
- [x] **SET-T020**: Implement `NetworkConfigScreen` with `DeviceConnectionStatus` fetch for WiFi/Ethernet state display
- File: `radio/component/NetworkConfigItemList.kt`
- Validates: FR-001, US-2
- [x] **SET-T021**: Implement `DisplayConfigScreen` for OLED/E-Ink display settings
- File: `radio/component/DisplayConfigItemList.kt`
- Validates: FR-001, US-2
- [x] **SET-T022**: Implement `BluetoothConfigScreen` for Bluetooth settings (filtered by `hasBluetooth` metadata)
- File: `radio/component/BluetoothConfigItemList.kt`
- Validates: FR-001, FR-006, US-2
---
## Phase 4 — Module Configuration
- [x] **SET-T023**: Implement `ModuleConfigurationScreen` with role-filtered, capability-filtered, excludability-filtered module route list and `unlockExcludedModules` support
- File: `ModuleConfigurationScreen.kt`
- Validates: FR-007, US-3
- [x] **SET-T024**: Implement MQTT config screen with `MQTTConfigItemList` and broker connection probe (`probeMqttConnection`, `MqttProbeStatus` display)
- File: `radio/component/MQTTConfigItemList.kt`
- Validates: FR-018, US-3
- [x] **SET-T025**: Implement Canned Message config screen with `getCannedMessages()` fetch and `setCannedMessages()` save
- File: `radio/component/CannedMessageConfigItemList.kt`
- Validates: FR-001, US-3
- [x] **SET-T026**: Implement External Notification config screen (expect/actual) with `getRingtone()` fetch and `setRingtone()` save
- File: `radio/component/ExternalNotificationConfigScreen.kt`
- Validates: FR-001, US-3
- [x] **SET-T027**: Implement remaining module config screens: Serial, Store & Forward, Range Test, Telemetry, Audio, Remote Hardware, Neighbor Info, Ambient Lighting, Detection Sensor, Paxcounter
- Files: `radio/component/{Serial,StoreForward,RangeTest,Telemetry,Audio,RemoteHardware,NeighborInfo,AmbientLighting,DetectionSensor,Paxcounter}ConfigItemList.kt`
- Validates: FR-001, US-3
- [x] **SET-T028**: Implement Status Message config screen (firmware capability gated via `supportsStatusMessage`)
- File: `radio/component/StatusMessageConfigItemList.kt`
- Validates: FR-007, US-3
- [x] **SET-T029**: Implement Traffic Management config screen (firmware capability gated via `supportsTrafficManagementConfig`)
- File: `radio/component/TrafficManagementConfigItemList.kt`
- Validates: FR-007, US-3
- [x] **SET-T030**: Implement TAK config screen (role-gated: `TAK` or `TAK_TRACKER` only) with `TakPermissionUtil` expect/actual
- Files: `radio/component/TAKConfigItemList.kt`, `tak/TakPermissionUtil.kt`
- Validates: FR-007, US-3
---
## Phase 5 — App Preferences
- [x] **SET-T031**: Implement `SettingsViewModel` with theme, locale, app intro, provide-location, DB cache limit, mesh log retention, and notification preference management via use cases
- File: `SettingsViewModel.kt`
- Validates: FR-010, FR-011, FR-012, US-4
- [x] **SET-T032**: Implement `PrivacySection` composable: analytics opt-in/out toggle, provide-location-to-mesh with GPS/permission checks, homoglyph encoding toggle
- File: `component/PrivacySection.kt`
- Validates: FR-010, US-4
- [x] **SET-T033**: Implement `NotificationSection` composable: messages, node events, low battery notification toggles
- File: `component/NotificationSection.kt`
- Validates: FR-011, US-4
- [x] **SET-T034**: Implement `ThemePickerDialog` for theme selection (light/dark/system)
- File: `component/ThemePickerDialog.kt`
- Validates: FR-010, US-4
- [x] **SET-T035**: Implement `HomoglyphSetting` composable for homoglyph character encoding toggle
- File: `component/HomoglyphSetting.kt`
- Validates: FR-010
- [x] **SET-T036**: Implement `MapReportingPreference` composable for position map reporting consent toggle
- File: `radio/component/MapReportingPreference.kt`
- Validates: FR-010
- [x] **SET-T037**: Implement MQTT proxy connection state display (`mqttConnectionState` flow) in settings UI
- File: `radio/RadioConfigViewModel.kt` (mqttConnectionState)
- Validates: FR-018
- [x] **SET-T038**: Implement `ExpressiveSection` reusable M3 section container with title styling
- File: `component/ExpressiveSection.kt`
- Validates: NFR-001, Design Standards
---
## Phase 6 — Administration & Advanced
- [x] **SET-T039**: Implement `AdministrationScreen` with `AdminRoute` enum (Reboot, Shutdown, Factory Reset, Node DB Reset), confirmation dialogs (`ShutdownConfirmationDialog`, `WarningDialog`), and metadata-aware shutdown guard
- Files: `AdministrationScreen.kt`, `radio/RadioConfig.kt` (AdminRoute), `radio/component/ShutdownConfirmationDialog.kt`, `radio/component/WarningDialog.kt`
- Validates: FR-004, FR-005, US-5
- [x] **SET-T040**: Implement admin action dispatch in `RadioConfigViewModel.sendAdminRequest()`: reboot, shutdown (with `canShutdown` guard), factory reset (with `isLocal` flag), node DB reset (with `preserveFavorites`)
- File: `radio/RadioConfigViewModel.kt`
- Validates: FR-004, FR-005, US-5
- [x] **SET-T041**: Implement profile import/export: `importProfile()` with file read and `DeviceProfile` parse, `exportProfile()` with file write, `installProfile()` with device application
- File: `radio/RadioConfigViewModel.kt`
- Validates: FR-008, US-6
- [x] **SET-T042**: Implement security config export: `exportSecurityConfig()` writing `SecurityConfig` protobuf to file
- File: `radio/RadioConfigViewModel.kt`
- Validates: FR-009
- [x] **SET-T043**: Implement `EditDeviceProfileDialog` for previewing and confirming imported profiles before installation
- File: `radio/component/EditDeviceProfileDialog.kt`
- Validates: FR-008, US-6
- [x] **SET-T044**: Implement `CleanNodeDatabaseViewModel` and `CleanNodeDatabaseScreen` for node database cleanup with age threshold and unknown-node filter
- Files: `radio/CleanNodeDatabaseViewModel.kt`, `radio/CleanNodeDatabaseScreen.kt`
- Validates: FR-016, US-9
- [x] **SET-T045**: Implement `RadioConfigItemList` composable organizing all settings sections (Radio Config, Device Config, Module Settings, Backup/Restore, Administration, Advanced) with managed device warnings and `LoadingOverlay` + `PacketResponseStateDialog`
- Files: `radio/RadioConfig.kt`, `radio/component/LoadingOverlay.kt`, `radio/component/PacketResponseStateDialog.kt`, `radio/component/RadioConfigScreenList.kt`
- Validates: FR-019, US-15
---
## Phase 7 — Debug Panel
- [x] **SET-T046**: Implement `DebugViewModel` with `UiMeshLog` model, mesh log observation, protobuf payload decoding (Position, Telemetry, Routing, AdminMessage, etc.), `LogSearchManager`, and `LogFilterManager`
- File: `debugging/DebugViewModel.kt`
- Validates: FR-013, FR-014, US-7
- [x] **SET-T047**: Implement `DebugScreen` composable with `LazyColumn`, sticky search/filter header, auto-scroll, selectable log items with decoded payloads, copy-to-clipboard, and annotated node IDs
- File: `debugging/Debug.kt`
- Validates: FR-013, FR-014, US-7
- [x] **SET-T048**: Implement `DebugSearchBar` and search state composables with multi-term regex search, match navigation (next/previous), match count display, and search highlighting
- File: `debugging/DebugSearch.kt`
- Validates: FR-013, US-7
- [x] **SET-T049**: Implement `DebugFilterBar` and filter composables with preset filters (node ID, broadcast, portnums, date), custom filter input, AND/OR mode toggle, and active filter chip display
- File: `debugging/DebugFilters.kt`
- Validates: FR-013, US-7
- [x] **SET-T050**: Implement `LogExporter` (expect/actual) for platform-specific log export to file with timestamped filename
- File: `debugging/LogExporter.kt`
- Validates: FR-013, US-7
- [x] **SET-T051**: Implement `LogFormatter` for mesh log message formatting
- File: `debugging/LogFormatter.kt`
- Validates: FR-014
---
## Phase 8 — Message Filtering & About
- [x] **SET-T052**: Implement `FilterSettingsScreen` with filter enable toggle, word add/remove, regex pattern support, and pattern rebuild
- File: `filter/FilterSettingsScreen.kt`
- Validates: FR-015, US-8
- [x] **SET-T053**: Implement `FilterSettingsViewModel` with `FilterPrefs` and `MessageFilter` integration for add/remove/toggle/rebuild
- File: `filter/FilterSettingsViewModel.kt`
- Validates: FR-015, US-8
- [x] **SET-T054**: Implement `AboutScreen` with open-source library acknowledgements via `AboutLibrariesLoader` (expect/actual)
- Files: `AboutScreen.kt`, `navigation/AboutLibrariesLoader.kt`
- Validates: Design Standards
- [x] **SET-T055**: Implement `PrefExporter` (expect/actual) for XML preference export for TAK integration
- File: `tak/PrefExporter.kt`
- [x] **SET-T056**: Implement shared utility files: `SettingsIntervals`, `FixedUpdateIntervals`, `Formatting`
- Files: `util/SettingsIntervals.kt`, `util/FixedUpdateIntervals.kt`, `util/Formatting.kt`
- [x] **SET-T057**: Implement CSV data export (`saveDataCsv`) with optional portnum filtering via `ExportDataUseCase`
- File: `SettingsViewModel.kt`
- Validates: FR-020
- [x] **SET-T058**: Implement platform-specific `SettingsMainScreen` (expect/actual) with Android `OutlinedCard` layout and Desktop layout
- Files: `navigation/SettingsNavigation.kt` (expect), `androidMain/`, `jvmMain/`, `iosMain/`
- Validates: NFR-003
---
## Phase 9 — Testing (Implemented)
- [x] **SET-T059**: Implement `RadioConfigViewModelTest` — 13 tests covering `setConfig`, `setOwner`, `setModuleConfig`, `updateChannels`, `setRingtone`, `setCannedMessages`, `setFixedPosition`, `removeFixedPosition`, `installProfile`, admin actions (reboot, shutdown, factory reset, node DB reset), packet response processing, request timeout, `initDestNum`, `setPreserveFavorites`, analytics toggle, homoglyph toggle
- File: `commonTest/radio/RadioConfigViewModelTest.kt` (535 lines)
- Validates: SC-002, SC-005, SC-007
- [x] **SET-T060**: Implement `SettingsViewModelTest` — 13 tests covering initialization, `isConnected` flow, `isOtaCapable`, notification settings, mesh log logging, `unlockExcludedModules`, `provideLocation` flow, mesh location use case calls, property-based bounds testing for retention days, theme/locale/app-intro prefs, `setDbCacheLimit` clamping
- File: `commonTest/SettingsViewModelTest.kt` (264 lines)
- Validates: SC-003, SC-007
- [x] **SET-T061**: Implement `DebugViewModelTest` — 6 tests covering retention days update, logging disable + log deletion, search filtering, AND/OR filter modes, preset filters, delete confirmation alert
- File: `commonTest/debugging/DebugViewModelTest.kt` (189 lines)
- Validates: SC-004, SC-007
- [x] **SET-T062**: Implement `DebugSearchTest` — 5 Compose UI tests covering search bar placeholder, clear button, match navigation arrows, filter bar display, custom filter add/display, clear-all filters
- File: `commonTest/debugging/DebugSearchTest.kt` (188 lines)
- Validates: SC-004, SC-007
- [x] **SET-T063**: Implement `CommonChannelViewModelTest` — 4 tests covering `isManaged` security config, `txEnabled`, share tracking, channel URL request parsing
- File: `commonTest/channel/CommonChannelViewModelTest.kt` (103 lines)
- Validates: SC-001, SC-007
- [x] **SET-T064**: Implement `FilterSettingsViewModelTest` — 3 tests covering `setFilterEnabled`, `addFilterWord` with pattern rebuild, `removeFilterWord` with pattern rebuild
- File: `commonTest/filter/FilterSettingsViewModelTest.kt` (72 lines)
- Validates: SC-007
- [x] **SET-T065**: Implement `EditDeviceProfileDialogTest` — profile dialog compose tests
- File: `commonTest/radio/component/EditDeviceProfileDialogTest.kt`
- Validates: SC-006
- [x] **SET-T066**: Implement `MapReportingPreferenceTest` — map consent preference tests
- File: `commonTest/radio/component/MapReportingPreferenceTest.kt`
- [x] **SET-T067**: Implement `CleanNodeDatabaseViewModelTest` — node database cleanup tests
- File: `commonTest/radio/CleanNodeDatabaseViewModelTest.kt`
- Validates: SC-007
- [x] **SET-T068**: Implement `LogFormatterTest` — log message formatting tests
- File: `commonTest/debugging/LogFormatterTest.kt`
- Validates: SC-004
---
## Phase 10 — Gap Tasks (Not Yet Implemented)
- [ ] **[DEFERRED]** **SET-T069**: Add Compose UI tests for `RadioConfigItemList` composable — verify section rendering, managed device message display, enabled/disabled state based on connection — *Deferred: requires Compose UI test infrastructure.*
- Target: `commonTest/radio/RadioConfigItemListTest.kt`
- Gap: No UI test coverage for the main radio config list
- [ ] **[DEFERRED]** **SET-T070**: Add Compose UI tests for `AdministrationScreen` — verify all admin route items render, confirmation dialogs appear on click, metadata-aware shutdown guard UX — *Deferred: requires Compose UI test infrastructure.*
- Target: `commonTest/AdministrationScreenTest.kt`
- Gap: No UI test for admin screen composable
- [ ] **[DEFERRED]** **SET-T071**: Add Compose UI tests for `FilterSettingsScreen` — verify filter enable toggle, word add/remove flow, regex indicator display — *Deferred: requires Compose UI test infrastructure.*
- Target: `commonTest/filter/FilterSettingsScreenTest.kt`
- Gap: Only ViewModel is tested, not the composable
- [ ] **[DEFERRED]** **SET-T072**: Add Compose UI tests for `CleanNodeDatabaseScreen` — verify slider interaction, preview list, confirm deletion flow — *Deferred: requires Compose UI test infrastructure.*
- Target: `commonTest/radio/CleanNodeDatabaseScreenTest.kt`
- Gap: Only ViewModel is tested, not the composable
- [x] **SET-T073**: Add integration test for profile import → export round-trip verifying `DeviceProfile` protobuf fidelity
- Target: `commonTest/radio/ProfileRoundTripTest.kt`
- Gap: Import and export are tested individually but not end-to-end
- [x] **SET-T074**: Add test for MQTT probe timeout and error path (`probeMqttConnection` exception handling, `clearMqttProbeStatus`)
- Target: `commonTest/radio/RadioConfigViewModelTest.kt` (extend)
- Gap: MQTT probe not tested
- [ ] **[DEFERRED]** **SET-T075**: Add accessibility tests — verify TalkBack semantics, touch target sizes, and color-independent information for admin action error colors — *Deferred: requires accessibility testing infrastructure (TalkBack, touch target verification).*
- Target: `commonTest/AdministrationAccessibilityTest.kt`
- Gap: No accessibility testing exists
- [x] **SET-T076**: Add test for `SettingsViewModel.saveDataCsv()` verifying CSV export through `FileService` and `ExportDataUseCase`
- Target: `commonTest/SettingsViewModelTest.kt` (extend)
- Gap: CSV export function exists but is not tested
---
## Summary
| Phase | Tasks | Completed | Gaps |
|-------|-------|-----------|------|
| 1. Foundation | 5 | 5 | 0 |
| 2. Radio Config | 10 | 10 | 0 |
| 3. Device Config | 7 | 7 | 0 |
| 4. Module Config | 8 | 8 | 0 |
| 5. App Preferences | 8 | 8 | 0 |
| 6. Administration & Advanced | 7 | 7 | 0 |
| 7. Debug Panel | 6 | 6 | 0 |
| 8. Filtering & About | 9 | 9 | 0 |
| 9. Testing (Implemented) | 10 | 10 | 0 |
| 10. Gap Tasks | 8 | 0 | 8 |
| **Total** | **78** | **70** | **8** |

View File

@@ -1,157 +0,0 @@
# Implementation Plan: Map View
**Branch**: `009-map-view` | **Date**: 2026-06-11 | **Spec**: `specs/009-map-view/spec.md`
**Input**: Feature specification from `/specs/009-map-view/spec.md`
**Note**: Brownfield migration — reverse-engineered from existing implementation.
## Summary
Map View provides an interactive map displaying mesh node positions, waypoints, traceroute overlays, and custom map layers. The shared `BaseMapViewModel` in `commonMain` manages node data flows, filter state, waypoint operations, and traceroute resolution. Platform-specific map rendering is delegated via composition locals (`LocalMapViewProvider`). The feature uses Koin for DI, Navigation 3 for routing, and Material 3 Expressive for the controls toolbar.
## Technical Context
**Language/Version**: Kotlin 2.3+ targeting JDK 21
**Primary Dependencies**: Compose Multiplatform, Material 3 Expressive, Koin 4.2+ (K2 Compiler Plugin), DataStore KMP, Navigation 3
**Storage**: DataStore KMP for map preferences (filter, favorites, waypoints visibility, precision circles, map style)
**Testing**: KMP `allTests` for `feature:map` commonTest; `testGoogleDebugUnitTest` for Android-specific tests
**Target Platform**: Android, Desktop (JVM), iOS — all via `commonMain` (map rendering platform-specific)
**Project Type**: Mobile/desktop app (Kotlin Multiplatform)
**Performance Goals**: Smooth map rendering with 100+ node markers; filter state changes reflected within 500ms
**Constraints**: All UI in `commonMain`; no `java.*`/`android.*` in common; CMP float pre-formatting via `NumberFormatter.format()`
**Scale/Scope**: 8 commonMain files, 1 androidMain file, 5 commonTest files, 2 androidUnitTest files
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Kotlin Multiplatform Core | ✅ PASS | All business logic in `commonMain`. `MapScreen.kt` in `androidMain` is a thin Scaffold host only. No `java.*`/`android.*` in common code. |
| II. Zero Lint Tolerance | ✅ PASS | `spotlessApply` + `detekt` pass. `detekt-baseline.xml` present for acknowledged suppressions. |
| III. Compose Multiplatform UI | ✅ PASS | Uses CMP composables (`HorizontalFloatingToolbar`, `FilledIconButton`, `Scaffold`). Map rendering delegated via composition local. |
| IV. Privacy First | ✅ PASS | No PII or location logging. Node positions from mesh, not phone GPS. Proto submodule read-only. |
| V. Design Standards Compliance | ✅ PASS | M3 Expressive toolbar, `MeshtasticIcons`, `stringResource()` for all labels. Content descriptions on all interactive elements. |
| VI. Verify Before Push | ✅ PASS | Full verification: `./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests`. |
| VII. Coroutine Safety | ✅ PASS | Uses `safeLaunch {}` with project `ioDispatcher`. No `runCatching {}` or `Dispatchers.IO` in common code. |
| VIII. Resource Discipline | ✅ PASS | `stringResource(Res.string.*)`, `MeshtasticIcons.*` throughout. |
| IX. Branch & Scope Hygiene | ✅ PASS | Feature scoped to `feature/map` module with clear boundaries. |
**Gate Result**: ✅ All principles satisfied. No violations requiring justification.
## Project Structure
### Documentation (this feature)
```text
specs/009-map-view/
├── spec.md # Feature specification (migrated)
├── plan.md # This file (migrated)
└── tasks.md # Task list (migrated)
```
### Source Code (repository root)
```text
feature/map/ ← Primary changes
├── src/commonMain/kotlin/org/meshtastic/feature/map/
│ ├── BaseMapViewModel.kt ← Shared ViewModel — nodes, waypoints, filters, traceroute
│ ├── SharedMapViewModel.kt ← Koin-injectable ViewModel (extends BaseMapViewModel)
│ ├── component/
│ │ ├── MapButton.kt ← Reusable FilledIconButton for map controls
│ │ └── MapControlsOverlay.kt ← M3 Expressive HorizontalFloatingToolbar
│ ├── di/
│ │ └── FeatureMapModule.kt ← Koin module with @ComponentScan
│ ├── model/
│ │ └── MapLayer.kt ← MapLayerItem data class, LayerType enum
│ ├── navigation/
│ │ └── MapNavigation.kt ← Navigation 3 graph entry for MapRoute
│ └── node/
│ └── NodeMapViewModel.kt ← Per-node position history ViewModel
├── src/androidMain/kotlin/org/meshtastic/feature/map/
│ └── MapScreen.kt ← Android Scaffold host with LocalMapViewProvider
├── src/commonTest/kotlin/org/meshtastic/feature/map/
│ ├── BaseMapViewModelTest.kt ← ViewModel initialization, connection state, node flow tests
│ ├── LastHeardFilterTest.kt ← Filter enum round-trip and edge case tests
│ ├── TracerouteNodeSelectionTest.kt ← Traceroute overlay resolution tests (8 test cases)
│ └── model/
│ ├── MapLayerTest.kt ← MapLayerItem defaults test
│ └── TracerouteOverlayTest.kt ← TracerouteOverlay route processing tests
└── src/androidUnitTestGoogle/kotlin/org/meshtastic/feature/map/
├── MapViewModelTest.kt ← Google-flavor ViewModel tests (tile providers, layers, waypoints)
└── MBTilesProviderTest.kt ← MBTiles TMS coordinate translation test
core/repository/ ← Dependencies (not modified)
├── MapPrefs ← DataStore-backed map preference interface
├── NodeRepository ← Node data access
└── PacketRepository ← Waypoint data access
core/model/ ← Dependencies (not modified)
├── Node ← Node data model
├── TracerouteOverlay ← Traceroute route data
├── DataPacket ← Waypoint container
└── RadioController ← Mesh radio interface
```
**Structure Decision**: The `feature/map` module follows the standard KMP feature module pattern. Business logic is in `commonMain`, platform-specific rendering is injected via composition locals. The `androidMain` source set contains only a thin `MapScreen` Scaffold host — actual map rendering (Google Maps / OSM) lives in build-flavor-specific modules outside this feature.
## Module Impact
| Module | Change Type | Files Affected | Risk |
|--------|-------------|----------------|------|
| `feature/map` (commonMain) | Existing | 8 | Low |
| `feature/map` (androidMain) | Existing | 1 | Low |
| `core/repository` | Read-only dependency | 0 | None |
| `core/model` | Read-only dependency | 0 | None |
| `core/ui` | Read-only dependency | 0 | None |
| `core/resources` | Read-only dependency | 0 (strings already exist) | None |
## Integration Points
- **Navigation**: `MapNavigation.mapGraph()` registers `MapRoute.Map` entry, navigates to `NodesRoute.NodeDetail` on node tap.
- **DI**: `FeatureMapModule` uses Koin `@ComponentScan` to discover `SharedMapViewModel` and `NodeMapViewModel`.
- **Map Rendering**: `LocalMapViewProvider.current?.MapView()` injected by build-flavor modules (Google / F-Droid).
- **Map Screen Host**: `LocalMapMainScreenProvider.current` injected for the main map screen composable.
- **Preferences**: `MapPrefs` interface from `core/repository` backed by DataStore.
- **Radio**: `RadioController` for sending waypoints and generating packet IDs.
## Design Constraints
- All UI lives in `commonMain` — not platform-specific
- Strings accessed via `stringResource(Res.string.key)` — never hardcoded
- Icons use `MeshtasticIcons` exclusively (from `core/ui/icon/`)
- Error handling uses `safeCatching {}` not `runCatching {}`
- Dispatchers via `org.meshtastic.core.common.util.ioDispatcher`
- Float values must be pre-formatted with `NumberFormatter.format()` (CMP constraint)
- Map rendering is platform-injected — `feature/map` has zero dependency on Google Maps SDK or OSM library
## Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| MapScreen in androidMain limits multiplatform reach | Medium | Medium | Thin host only; actual rendering via composition local. Desktop/iOS provide their own MapView implementations. |
| Missing Compose UI tests for controls overlay | Low | Low | Manual testing covers; unit tests cover ViewModel logic comprehensively. |
| Waypoint expiration edge cases (timezone, clock skew) | Low | Medium | Uses `nowSeconds` utility; expiration logic has clear boundary checks. |
## Phase Alignment with Tasks
| Phase | Purpose | Key Tasks | Dependencies |
|-------|---------|-----------|--------------|
| 1. Core ViewModel & Models | Data layer and business logic | MAP-T001MAP-T007 | None |
| 2. UI Components | Map controls and composables | MAP-T008MAP-T012 | Phase 1 |
| 3. Navigation & DI | Routing and dependency injection | MAP-T013MAP-T014 | Phase 2 |
| 4. Testing | Unit and integration tests | MAP-T015MAP-T022 | Phase 13 |
### Critical Path
```
Phase 1 (ViewModel + Models) → Phase 2 (UI Components) → Phase 3 (Navigation + DI) → Phase 4 (Tests)
```
## Complexity Tracking
> **Fill ONLY if Constitution Check has violations that must be justified**
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| *None* | — | — |

View File

@@ -1,229 +0,0 @@
# Feature Specification: Map View
**Feature Branch**: `009-map-view`
**Created**: 2026-06-11
**Status**: Migrated
**Input**: Brownfield migration — reverse-engineered from existing `feature/map/` module
## Summary
Map View displays Meshtastic mesh node positions on an interactive map, overlays waypoints, supports traceroute visualization, and provides filtering/preference controls. The feature uses a shared `BaseMapViewModel` in `commonMain` with platform-specific map rendering delegated via `LocalMapViewProvider` (Google Maps on Android/Google, OSM on F-Droid). Users can filter nodes by last-heard time, toggle favorites-only mode, show/hide waypoints and precision circles, manage custom tile layers (KML/GeoJSON), and visualize traceroute paths with snapshot positions.
## Goals
1. Display all mesh nodes with valid GPS positions as markers on an interactive map.
2. Allow users to filter visible nodes by last-heard time window and favorites-only mode.
3. Render waypoints (with automatic expiration) and allow creation/deletion of waypoints from the map.
4. Visualize traceroute paths between nodes, using historical snapshot positions when available.
5. Support custom map layers (KML, GeoJSON) from local files and network URLs.
## Non-Goals
- Offline map tile caching or download management (handled by platform map providers).
- Turn-by-turn navigation or routing between nodes.
- Modifying node positions — the map is read-only for position data.
- Real-time GPS streaming from the phone to the mesh (handled by `core/service`).
## User Scenarios & Testing *(mandatory)*
### User Story 1 — View Node Positions on Map (Priority: P1)
A Meshtastic user opens the Map tab to see where all mesh nodes are located. Nodes with valid GPS positions appear as markers on the map. The user's own node is highlighted, and tapping a node marker navigates to that node's detail screen. Ignored nodes are excluded from display.
**Why this priority**: Core value proposition — the map exists to show node locations.
**Independent Test**: Connect to a mesh with 3+ nodes that have GPS positions; open the Map tab; verify each node appears at its reported coordinates. Tap a marker and verify navigation to Node Detail.
**Acceptance Scenarios**:
1. **Given** a mesh with 5 nodes where 3 have GPS positions, **When** the user opens the Map tab, **Then** exactly 3 node markers are displayed (nodes without positions are omitted).
2. **Given** a node is marked as "ignored," **When** the map renders, **Then** that node's marker is not shown.
3. **Given** the device is connected, **When** the user views the Map screen, **Then** the top app bar shows the connected node chip; tapping the chip navigates to the node's detail.
4. **Given** a node marker is visible, **When** the user taps it, **Then** the app navigates to `NodesRoute.NodeDetail(id)`.
---
### User Story 2 — Filter Nodes by Last Heard Time (Priority: P1)
A user with a large mesh wants to focus on recently active nodes. They open the map filter controls and select a last-heard window (1 hour, 8 hours, 1 day, 2 days, or "Any"). Only nodes heard within the selected window remain visible on the map. The filter preference persists across sessions.
**Why this priority**: Essential for usability on large meshes with stale nodes.
**Independent Test**: Set filter to "1 Hour"; verify only nodes heard within the last hour are shown. Change to "Any"; verify all nodes reappear. Restart app; verify filter persists.
**Acceptance Scenarios**:
1. **Given** the last-heard filter is set to "One Hour," **When** a node was last heard 2 hours ago, **Then** that node's marker is hidden.
2. **Given** the filter is "Any" (0 seconds), **When** the map renders, **Then** all nodes with valid positions are shown regardless of last-heard time.
3. **Given** an unknown seconds value is loaded from preferences, **When** `LastHeardFilter.fromSeconds()` is called, **Then** it defaults to `Any`.
4. **Given** the user changes the filter, **When** the app is relaunched, **Then** the previously selected filter is restored from `MapPrefs`.
---
### User Story 3 — Display and Manage Waypoints (Priority: P2)
A user creates or receives waypoints on the mesh. The map displays active (non-expired) waypoints as distinct markers. Users can send new waypoints and delete existing ones. Expired waypoints are automatically hidden.
**Why this priority**: Waypoints are a core mesh feature for marking points of interest, but secondary to node display.
**Independent Test**: Send a waypoint with a future expiration; verify it appears on the map. Wait for expiration (or set past expiration); verify it disappears. Delete a waypoint; verify removal.
**Acceptance Scenarios**:
1. **Given** waypoints exist with `expire = 0` (never expires), **When** the map renders with "Show Waypoints" enabled, **Then** those waypoints appear as markers.
2. **Given** a waypoint has `expire` in the past, **When** the map renders, **Then** that waypoint is excluded.
3. **Given** "Show Waypoints" is toggled off, **When** the map renders, **Then** no waypoint markers are displayed.
4. **Given** the user deletes a waypoint, **When** `deleteWaypoint(id)` is called, **Then** the waypoint is removed from the packet repository.
5. **Given** the user creates a waypoint, **When** `sendWaypoint(wpt, contactKey)` is called with a valid ID, **Then** the waypoint is transmitted via `RadioController`.
---
### User Story 4 — Visualize Traceroute Paths (Priority: P2)
A user runs a traceroute to another node and wants to see the path on the map. The map overlays the forward and return routes as polylines, with markers at each hop. When snapshot positions (recorded at traceroute time) are available, those are used instead of live positions for accurate historical visualization.
**Why this priority**: Traceroute visualization helps diagnose mesh topology issues, but is an advanced feature.
**Independent Test**: Run a traceroute between two nodes with intermediate hops; verify polyline and hop markers appear. Verify snapshot positions override live positions when available.
**Acceptance Scenarios**:
1. **Given** a traceroute overlay with `forwardRoute = [10, 20]`, **When** snapshot positions exist for nodes 10 and 20, **Then** markers use snapshot coordinates, not live DB positions.
2. **Given** a traceroute overlay with no snapshot positions, **When** the map renders, **Then** markers fall back to live node positions filtered to overlay node nums.
3. **Given** a traceroute overlay with empty routes, **When** `tracerouteNodeSelection()` is called, **Then** `overlayNodeNums` and `nodesForMarkers` are both empty.
4. **Given** a snapshot includes node 30 not in the overlay routes, **When** markers are rendered, **Then** node 30 appears in `nodeLookup` (for polylines) but not in `nodesForMarkers`.
---
### User Story 5 — Map Controls and Preferences (Priority: P2)
A user interacts with the map toolbar to control compass orientation, toggle location tracking, switch map types/tile sources, manage custom overlay layers (KML/GeoJSON), and toggle favorites-only mode and precision circles. All preferences persist across sessions.
**Why this priority**: Controls enhance usability but are not required for basic map viewing.
**Independent Test**: Toggle each control (compass, location tracking, favorites, precision circles, waypoints); verify visual feedback and persistence after app restart.
**Acceptance Scenarios**:
1. **Given** the compass button is clicked, **When** the map bearing is non-zero, **Then** the map rotates to north and the compass icon color changes.
2. **Given** "Show Only Favorites" is toggled on, **When** the map re-renders, **Then** only favorited nodes are shown.
3. **Given** "Show Precision Circle" is toggled on, **When** nodes have precision data, **Then** precision circles render around node markers.
4. **Given** the user adds a network map layer with a `.geojson` URL, **When** the layer is added, **Then** it is detected as `LayerType.GEOJSON`.
5. **Given** the user adds a layer with a `.kml` URL, **When** the layer is added, **Then** it defaults to `LayerType.KML`.
---
### Edge Cases
- What happens when no nodes have GPS positions? The map renders empty with controls still functional.
- What happens when the device is disconnected? `isConnected` becomes `false`; the node chip is hidden from the app bar but existing markers remain.
- What happens when a waypoint `expire` field is `0`? It is treated as "never expires" and always shown.
- What happens when `contactKey` in `sendWaypoint` has no leading digit? The entire key is used as `dest` with channel defaulting to the key itself.
- What happens when `LastHeardFilter.fromSeconds()` receives a negative value? It defaults to `Any`.
## Architecture
### Key Components
| Component | Module / File | Purpose |
|-----------|---------------|---------|
| `BaseMapViewModel` | `feature/map/src/commonMain/.../BaseMapViewModel.kt` | Shared ViewModel with node data, waypoints, filters, traceroute logic |
| `SharedMapViewModel` | `feature/map/src/commonMain/.../SharedMapViewModel.kt` | Koin-injectable ViewModel extending `BaseMapViewModel` |
| `NodeMapViewModel` | `feature/map/src/commonMain/.../node/NodeMapViewModel.kt` | Per-node map with position history log |
| `MapControlsOverlay` | `feature/map/src/commonMain/.../component/MapControlsOverlay.kt` | M3 Expressive floating toolbar with compass, filter, location controls |
| `MapButton` | `feature/map/src/commonMain/.../component/MapButton.kt` | Reusable `FilledIconButton` for map controls |
| `MapLayerItem` | `feature/map/src/commonMain/.../model/MapLayer.kt` | Data model for KML/GeoJSON overlay layers |
| `MapNavigation` | `feature/map/src/commonMain/.../navigation/MapNavigation.kt` | Navigation 3 graph entry for the map route |
| `FeatureMapModule` | `feature/map/src/commonMain/.../di/FeatureMapModule.kt` | Koin DI module with component scan |
| `MapScreen` | `feature/map/src/androidMain/.../MapScreen.kt` | Android Scaffold host delegating to `LocalMapViewProvider` |
| `LastHeardFilter` | `feature/map/src/commonMain/.../BaseMapViewModel.kt` | Enum for time-window filtering (Any, 1h, 8h, 1d, 2d) |
| `TracerouteNodeSelection` | `feature/map/src/commonMain/.../BaseMapViewModel.kt` | Data class resolving traceroute overlays to displayable nodes |
### Data Flow
```mermaid
graph TD
A[NodeRepository] -->|nodes flow| B[BaseMapViewModel]
C[PacketRepository] -->|waypoints flow| B
D[RadioController] -->|connectionState| B
E[MapPrefs] -->|filter prefs| B
B -->|nodesWithPosition| F[MapScreen / MapView]
B -->|waypoints| F
B -->|mapFilterState| F
B -->|tracerouteNodeSelection| F
F -->|onClickNode| G[NodesRoute.NodeDetail]
```
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST display all non-ignored nodes with valid GPS positions as markers on the map.
- **FR-002**: System MUST filter out nodes without valid positions from the map display (`nodesWithPosition` flow).
- **FR-003**: System MUST filter out ignored nodes from the map display.
- **FR-004**: System MUST support filtering nodes by last-heard time window with options: Any (0s), 1 Hour (3600s), 8 Hours (28800s), 1 Day (86400s), 2 Days (172800s).
- **FR-005**: System MUST persist last-heard filter, favorites-only, show-waypoints, and precision-circle preferences via `MapPrefs`.
- **FR-006**: System MUST display active (non-expired) waypoints on the map; waypoints with `expire > 0` and `expire <= now` MUST be excluded.
- **FR-007**: System MUST support sending waypoints to the mesh via `RadioController.sendMessage()`.
- **FR-008**: System MUST support deleting waypoints via `PacketRepository.deleteWaypoint()`.
- **FR-009**: System MUST resolve traceroute overlays into `TracerouteNodeSelection` using snapshot positions when available, falling back to live positions.
- **FR-010**: System MUST provide a compass button that rotates with map bearing and resets to north on click.
- **FR-011**: System MUST provide a location tracking toggle button switching between `MyLocation` and `LocationDisabled` icons.
- **FR-012**: System MUST support custom map overlay layers of type KML or GeoJSON, identifiable by file extension.
- **FR-013**: System MUST support per-node position history display via `NodeMapViewModel` using `MeshLogRepository` position packets, with deduplication by time or coordinates.
- **FR-014**: System MUST navigate to `NodesRoute.NodeDetail` when a node marker or app bar chip is tapped.
### Non-Functional Requirements
- **NFR-001**: Map controls overlay MUST use Material 3 Expressive `HorizontalFloatingToolbar` for consistent cross-flavor styling.
- **NFR-002**: All business logic and shared UI MUST reside in `commonMain`; platform-specific map rendering is provided via `LocalMapViewProvider`.
- **NFR-003**: ViewModel coroutines MUST use `safeLaunch` with `ioDispatcher` for IO operations.
- **NFR-004**: Strings MUST use `stringResource(Res.string.*)` — no hardcoded text.
- **NFR-005**: Icons MUST use `MeshtasticIcons` from `core/ui/icon/`.
## Source-Set Impact
| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | 8 files (ViewModels, components, models, DI, navigation) | All business logic and shared UI |
| `androidMain` | 1 file (`MapScreen.kt`) | Scaffold host — delegates rendering to platform provider |
| `androidUnitTestGoogle` | 2 files | Google Maps-specific ViewModel and MBTiles tests |
| `commonTest` | 5 files | Shared ViewModel, filter, traceroute, and model tests |
## Design Standards Compliance
- [x] New screens reviewed against design standards — `MapControlsOverlay` uses M3 Expressive `HorizontalFloatingToolbar`
- [x] M3 component selection verified — `FilledIconButton`, `CircularProgressIndicator`, `Scaffold`
- [x] Accessibility: compass has content descriptions; control buttons have semantic labels
- [x] Typography: app bar uses `MainAppBar` component from `core:ui`
## Privacy Assessment
- [x] No PII, location data, or cryptographic keys logged or exposed — node positions come from the mesh, not the phone's GPS
- [x] No new network calls that transmit user data — network layers are user-initiated URL fetches only
- [x] Proto submodule (`core/proto`) not modified (read-only upstream)
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: All non-ignored nodes with valid positions render as markers on the map within 1 second of screen load.
- **SC-002**: Last-heard filter correctly hides/shows nodes within 500ms of filter change.
- **SC-003**: Waypoints with past expiration are never displayed on the map.
- **SC-004**: Traceroute overlay uses snapshot positions when available, verified by unit tests asserting coordinate values.
- **SC-005**: `LastHeardFilter.fromSeconds()` returns `Any` for all unknown input values, verified by unit tests.
- **SC-006**: All map preferences persist across app restarts via `MapPrefs`.
- **SC-007**: Navigation from map marker to Node Detail completes successfully.
- **SC-008**: Custom map layers correctly detect KML vs. GeoJSON by file extension.
## Assumptions
- All business logic and UI composables reside in `commonMain` source set.
- String resources added to `core/resources/src/commonMain/composeResources/values/strings.xml`.
- Icons use `MeshtasticIcons` (from `core/ui/icon/`).
- Float values pre-formatted with `NumberFormatter.format()` (CMP constraint).
- Platform-specific map rendering (Google Maps / OSM) is provided via `LocalMapViewProvider` and `LocalMapMainScreenProvider` composition locals — the `feature/map` module does not depend on any specific map SDK directly.
- Waypoint IDs are unique integers generated by `RadioController.getPacketId()`.
- `TracerouteOverlay` and `Node` models are defined in `core/model`.
- `MapPrefs` is defined in `core/repository` and backed by DataStore.

View File

@@ -1,108 +0,0 @@
# Tasks: Map View
**Input**: Reverse-engineered from existing `feature/map/` module
**Prerequisites**: plan.md (required), spec.md (required)
**Tests**: Included — existing tests migrated; gap tasks added for missing coverage.
**Organization**: Tasks grouped by implementation phase. All existing work marked `[x]`; identified gaps marked `[ ]`.
**Status**: Migrated — all `[x]` tasks reflect code that already exists in the codebase.
## Format: `[MAP-TXXX] [P?] [Story?] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3, US4, US5)
- Include exact file paths in descriptions
## Path Conventions
- **KMP commonMain**: `feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/`
- **KMP commonTest**: `feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/`
- **Android source**: `feature/map/src/androidMain/kotlin/org/meshtastic/feature/map/`
- **Android tests**: `feature/map/src/androidUnitTestGoogle/kotlin/org/meshtastic/feature/map/`
- **Core deps**: `core/repository/`, `core/model/`, `core/ui/`
---
## Phase 1: Core ViewModel & Models
**Purpose**: Shared business logic for node data, waypoints, filters, traceroute resolution, and map layer models.
- [x] MAP-T001 [P] [US1] Create `BaseMapViewModel` in `feature/map/src/commonMain/.../BaseMapViewModel.kt` — shared ViewModel exposing `nodes`, `nodesWithPosition`, `myNodeInfo`, `ourNodeInfo`, `isConnected` flows from `NodeRepository` and `RadioController`. Filter ignored nodes from the `nodes` flow. (FR-001, FR-002, FR-003)
- [x] MAP-T002 [P] [US2] Implement `LastHeardFilter` enum in `BaseMapViewModel.kt` with entries `Any` (0s), `OneHour` (3600s), `EightHours` (28800s), `OneDay` (86400s), `TwoDays` (172800s). Include `fromSeconds()` companion factory defaulting to `Any` for unknown values. Wire `lastHeardFilter` and `lastHeardTrackFilter` state flows with `MapPrefs` persistence. (FR-004, FR-005)
- [x] MAP-T003 [P] [US3] Implement waypoint data flow in `BaseMapViewModel``waypoints: StateFlow<Map<Int, DataPacket>>` from `PacketRepository.getWaypoints()`, filtering expired waypoints using `nowSeconds`. Implement `deleteWaypoint(id)` and `sendWaypoint(wpt, contactKey)` methods using `safeLaunch` with `ioDispatcher`. (FR-006, FR-007, FR-008)
- [x] MAP-T004 [P] [US5] Implement map filter toggles in `BaseMapViewModel``showOnlyFavorites`, `showWaypointsOnMap`, `showPrecisionCircleOnMap` as `StateFlow<Boolean>` backed by `MapPrefs`. Combine all into `MapFilterState` data class via `mapFilterStateFlow`. (FR-005)
- [x] MAP-T005 [P] [US4] Implement `TracerouteNodeSelection` data class and `tracerouteNodeSelection()` top-level function in `BaseMapViewModel.kt` — resolve overlay node nums to displayable `Node` instances, prioritizing snapshot positions over live positions. Include convenience extension for `BaseMapViewModel`. (FR-009)
- [x] MAP-T006 [P] [US1] Create `SharedMapViewModel` in `feature/map/src/commonMain/.../SharedMapViewModel.kt` — Koin-injectable `@KoinViewModel` extending `BaseMapViewModel` with pass-through constructor. (FR-001)
- [x] MAP-T007 [P] [US1,US5] Create `MapLayerItem` data class and `LayerType` enum in `feature/map/src/commonMain/.../model/MapLayer.kt` — support KML and GeoJSON layer types with UUID-based IDs, visibility toggle, network flag, and refresh state. (FR-012)
**Dependencies**: None — all tasks are independent.
**Checkpoint**: Core data layer complete. All flows, filters, and models ready for UI consumption.
---
## Phase 2: UI Components
**Purpose**: Map controls overlay and reusable button composables.
- [x] MAP-T008 [P] [US5] Create `MapButton` composable in `feature/map/src/commonMain/.../component/MapButton.kt``FilledIconButton` wrapper accepting `ImageVector`, `contentDescription`, `onClick`, optional `iconTint`. Uses `IconButtonDefaults.filledIconButtonColors()`. (NFR-001, NFR-005)
- [x] MAP-T009 [US5] Create `MapControlsOverlay` composable in `feature/map/src/commonMain/.../component/MapControlsOverlay.kt``HorizontalFloatingToolbar` (M3 Expressive) containing compass button, filter button with dropdown slot, map type slot, layers slot, optional refresh button with `CircularProgressIndicator`, and location tracking toggle. (FR-010, FR-011, NFR-001)
- [x] MAP-T010 [US5] Implement `CompassButton` private composable within `MapControlsOverlay.kt` — rotates icon by `-bearing` degrees, uses `StatusRed` when north-aligned, `primary` when following phone bearing. (FR-010)
- [x] MAP-T011 [US1] Create `MapScreen` composable in `feature/map/src/androidMain/.../MapScreen.kt``Scaffold` with `MainAppBar` showing connected node chip, delegating map content to `LocalMapViewProvider.current?.MapView()`. (FR-014, NFR-002)
- [x] MAP-T012 [P] [US1] Create `NodeMapViewModel` in `feature/map/src/commonMain/.../node/NodeMapViewModel.kt` — per-node map ViewModel with `destNum` from `SavedStateHandle`, `node` flow from `NodeRepository`, `positionLogs` flow from `MeshLogRepository` with time/coordinate deduplication. (FR-013)
**Dependencies**: Phase 1 (MAP-T001MAP-T007) must complete first.
**Checkpoint**: All UI components and ViewModels implemented.
---
## Phase 3: Navigation & DI
**Purpose**: Wire feature into app navigation graph and dependency injection.
- [x] MAP-T013 [US1] Create `MapNavigation.mapGraph()` in `feature/map/src/commonMain/.../navigation/MapNavigation.kt` — register `MapRoute.Map` entry using Navigation 3 `EntryProviderScope`, resolve map screen via `LocalMapMainScreenProvider`, navigate to `NodesRoute.NodeDetail` on node tap. (FR-014)
- [x] MAP-T014 [P] Create `FeatureMapModule` in `feature/map/src/commonMain/.../di/FeatureMapModule.kt` — Koin `@Module` with `@ComponentScan("org.meshtastic.feature.map")` for automatic ViewModel discovery.
**Dependencies**: Phase 2 (MAP-T011, MAP-T012) must complete first.
**Checkpoint**: Feature fully wired into app navigation and DI.
---
## Phase 4: Testing
**Purpose**: Unit tests for ViewModels, models, and business logic. Includes existing tests and identified gaps.
- [x] MAP-T015 [P] [US1] Create `BaseMapViewModelTest` in `feature/map/src/commonTest/.../BaseMapViewModelTest.kt` — test initialization, `myNodeInfo` flow (starts null), `nodesWithPosition` flow (starts empty), `isConnected` flow (tracks `ConnectionState` changes), node repository integration. Uses Mokkery mocks for `MapPrefs` and `PacketRepository`, `FakeNodeRepository` and `FakeRadioController` for fakes. (SC-001)
- [x] MAP-T016 [P] [US2] Create `LastHeardFilterTest` in `feature/map/src/commonTest/.../LastHeardFilterTest.kt` — test `fromSeconds()` with all known values (0, 3600, 28800, 86400, 172800), unknown values (9999, -1, Long.MAX_VALUE default to `Any`), and `seconds` property round-trip. (SC-005)
- [x] MAP-T017 [P] [US4] Create `TracerouteNodeSelectionTest` in `feature/map/src/commonTest/.../TracerouteNodeSelectionTest.kt` — 8 test cases: null overlay returns all nodes, node lookup filters to valid positions, overlay with snapshot uses snapshot coordinates, snapshot node lookup, snapshot filters to overlay nodes, overlay without snapshot falls back to live nodes, empty overlay routes yield empty selection, getNodeOrFallback invocation verification. (SC-004)
- [x] MAP-T018 [P] [US5] Create `MapLayerTest` in `feature/map/src/commonTest/.../model/MapLayerTest.kt` — test `MapLayerItem` default values (auto-generated ID, null URI, visible=true, isNetwork=false, isRefreshing=false). (SC-008)
- [x] MAP-T019 [P] [US4] Create `TracerouteOverlayTest` in `feature/map/src/commonTest/.../model/TracerouteOverlayTest.kt` — test empty routes (`relatedNodeNums` empty, `hasRoutes` false) and populated routes (`relatedNodeNums` union, `hasRoutes` true).
- [x] MAP-T020 [P] [US5] Create `MapViewModelTest` in `feature/map/src/androidUnitTestGoogle/.../MapViewModelTest.kt` — Google-flavor tests: `getTileProvider` returns `UrlTileProvider` for remote config, `addNetworkMapLayer` detects GeoJSON by extension, KML default for other extensions, `setWaypointId` updates and clears value. Uses Robolectric. (SC-008)
- [x] MAP-T021 [P] [US5] Create `MBTilesProviderTest` in `feature/map/src/androidUnitTestGoogle/.../MBTilesProviderTest.kt` — test TMS y-coordinate translation (`y_tms = (1 << zoom) - 1 - y_google`), tile retrieval from SQLite database. Uses Robolectric with `TemporaryFolder`.
**Dependencies**: Phase 13 must complete first (tests exercise the full feature).
**Checkpoint**: All existing tests passing. Gaps identified below.
---
## Phase 5: Gap Tasks (Not Yet Implemented) ⚠️
**Purpose**: Address identified coverage gaps in the existing implementation.
- [x] MAP-T022 [US3] **[GAP]** Add unit tests for waypoint expiration filtering logic in `BaseMapViewModel` — test that waypoints with `expire > nowSeconds` are included, `expire <= nowSeconds` are excluded, and `expire == 0` (never expires) are always included. File: `feature/map/src/commonTest/.../BaseMapViewModelTest.kt`. (SC-003)
- [ ] **[DEFERRED]** MAP-T023 [US1,US5] **[GAP]** Add Compose UI tests for `MapControlsOverlay` and `MapButton` composables — verify compass rotation, filter button click, location tracking toggle icon switch, refresh spinner visibility. File: `feature/map/src/commonTest/.../component/MapControlsOverlayTest.kt`. (NFR-001) — *Deferred: requires Compose UI test infrastructure.*
**Dependencies**: Phase 4 testing infrastructure.
**Checkpoint**: Full test coverage achieved.
---
## Summary
| Phase | Tasks | Status |
|-------|-------|--------|
| Phase 1: Core ViewModel & Models | MAP-T001MAP-T007 (7 tasks) | ✅ All complete |
| Phase 2: UI Components | MAP-T008MAP-T012 (5 tasks) | ✅ All complete |
| Phase 3: Navigation & DI | MAP-T013MAP-T014 (2 tasks) | ✅ All complete |
| Phase 4: Testing | MAP-T015MAP-T021 (7 tasks) | ✅ All complete |
| Phase 5: Gap Tasks | MAP-T022MAP-T023 (2 tasks) | ⚠️ Not started |
| **Total** | **23 tasks** | **21/23 complete** |

View File

@@ -1,135 +0,0 @@
# Implementation Plan: Onboarding / Intro Flow
**Branch**: `010-onboarding` | **Date**: 2026-05-09 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/010-onboarding/spec.md`
**Status**: Migrated — reverse-engineered from existing implementation
## Summary
The onboarding intro flow is a 5-step linear wizard (Welcome → Bluetooth → Location → Notifications → Critical Alerts) that introduces first-time users to Meshtastic and requests runtime permissions. Navigation logic lives in `commonMain` via `IntroViewModel`; UI screens and platform permission handling live in `androidMain` using Accompanist Permissions and Navigation 3.
## Technical Context
**Language/Version**: Kotlin 2.3+ targeting JDK 21
**Primary Dependencies**: Compose Multiplatform, Material 3, Koin 4.2+ (K2 Compiler Plugin), Accompanist Permissions, Navigation 3
**Storage**: N/A (no local persistence — intro completion state managed by caller)
**Testing**: KMP `allTests` for `feature:intro` module
**Target Platform**: Android (UI), Desktop/iOS (commonMain logic only)
**Project Type**: Mobile/desktop app (Kotlin Multiplatform)
**Performance Goals**: Instant screen transitions; no network calls
**Constraints**: All UI in `androidMain` due to Accompanist Permissions dependency; ViewModel logic in `commonMain`
**Scale/Scope**: 3 `commonMain` files, 8 `androidMain` files, 1 test file — ~1,150 total lines
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Kotlin Multiplatform Core | ✅ PASS | ViewModel and NavKeys in `commonMain`. UI screens in `androidMain` due to platform permission APIs — acceptable per source-set boundaries. |
| II. Zero Lint Tolerance | ✅ PASS | Module compiles with `spotlessCheck` and `detekt` passing. |
| III. Compose Multiplatform UI | ✅ PASS | Uses `MeshtasticNavDisplay`, Navigation 3 `entryProvider`, `@Serializable data object` NavKeys. |
| IV. Privacy First | ✅ PASS | No PII logged. No network calls. Proto submodule untouched. |
| V. Design Standards Compliance | ✅ PASS | M3 Typography, `Scaffold`, `BottomAppBar` used throughout. `MeshtasticIcons` for all icons. |
| VI. Verify Before Push | ✅ PASS | `./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests` passes. |
| VII. Coroutine Safety | ✅ PASS | No coroutine/suspend code in this feature — ViewModel is synchronous. |
| VIII. Resource Discipline | ✅ PASS | All strings via `stringResource(Res.string.*)`. All icons via `MeshtasticIcons`. |
| IX. Branch & Scope Hygiene | ✅ PASS | Feature is self-contained in `feature/intro` module with clear boundaries. |
**Gate Result**: ✅ All principles satisfied
## Project Structure
### Documentation (this feature)
```text
specs/010-onboarding/
├── spec.md # Feature specification (migrated)
├── plan.md # This file (migrated)
└── tasks.md # Task breakdown (migrated)
```
### Source Code (repository root)
```text
feature/intro/ ← Primary module
├── build.gradle.kts ← KMP feature plugin + serialization
├── src/commonMain/kotlin/org/meshtastic/feature/intro/
│ ├── IntroNavKeys.kt ← @Serializable NavKey data objects
│ ├── IntroViewModel.kt ← Navigation step logic (getNextKey)
│ └── di/
│ └── FeatureIntroModule.kt ← Koin @Module with @ComponentScan
├── src/androidMain/kotlin/org/meshtastic/feature/intro/
│ ├── AppIntroductionScreen.kt ← Root composable, permission state hoisting
│ ├── IntroNavGraph.kt ← Navigation 3 entry provider
│ ├── WelcomeScreen.kt ← Welcome step with feature highlights
│ ├── BluetoothScreen.kt ← Bluetooth permission step
│ ├── LocationScreen.kt ← Location permission step
│ ├── NotificationsScreen.kt ← Notification permission step
│ ├── CriticalAlertsScreen.kt ← DND / critical alerts step
│ ├── PermissionScreenLayout.kt ← Reusable permission screen scaffold
│ ├── IntroBottomBar.kt ← Skip/Configure bottom bar
│ ├── IntroUiHelpers.kt ← FeatureRow + clickable annotated strings
│ └── FeatureUIData.kt ← Data class for feature row content
└── src/commonTest/kotlin/org/meshtastic/feature/intro/
└── IntroViewModelTest.kt ← 6 navigation flow tests
core/ui/component/MeshtasticNavDisplay.kt ← Reused — navigation display wrapper
core/ui/icon/MeshtasticIcons.kt ← Reused — project icon set
core/resources/src/commonMain/composeResources/ ← Reused — string resources
```
**Structure Decision**: The feature follows the standard `feature/*` KMP module pattern. UI screens are in `androidMain` because they depend on Accompanist Permissions and Android Intent APIs. The ViewModel and navigation keys are correctly in `commonMain` for cross-platform reuse.
## Module Impact
| Module | Change Type | Files Affected | Risk |
|--------|-------------|----------------|------|
| `feature/intro` | Existing (complete) | 12 source + 1 test | Low |
| `core/ui` | Reused (no changes) | 0 | None |
| `core/resources` | Reused (strings added) | 1 (strings.xml) | Low |
## Integration Points
- **App-level routing**: The app module (or root navigation) conditionally presents `AppIntroductionScreen` on first launch and handles the `onDone` callback to persist completion and navigate to the main app.
- **Koin DI**: `FeatureIntroModule` is registered in the app's Koin configuration. `IntroViewModel` is injected via `@KoinViewModel`.
- **Analytics**: `LocalAnalyticsIntroProvider` composition local is provided by the app module; the Welcome screen invokes it for opt-in display.
- **Notification Channel**: The `"my_alerts"` channel ID is referenced by the CriticalAlerts screen but must be pre-created elsewhere.
## Design Constraints
- All UI lives in `androidMain` — platform permission APIs require it
- Strings accessed via `stringResource(Res.string.key)` — never hardcoded
- Icons use `MeshtasticIcons` exclusively (from `core/ui/icon/`)
- No coroutine code — ViewModel is purely synchronous
- Navigation uses Navigation 3 `entryProvider` pattern with `rememberNavBackStack`
- Permission screens adapt button text based on grant state (`showNextButton` flag)
## Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| UI screens can't compile on Desktop/iOS | High | Medium | Screens are in `androidMain`; only ViewModel is cross-platform. Migration to CMP permissions API is a future task. |
| Notification channel `"my_alerts"` not found | Low | Low | Channel must be created by app module; verify in integration test. |
| Accompanist Permissions deprecated | Medium | Medium | Google has signaled Accompanist may be absorbed into core; monitor and migrate when alternatives are stable. |
## Phase Alignment with Tasks
| Phase | Purpose | Key Tasks | Dependencies |
|-------|---------|-----------|--------------|
| 1. Core Model & Navigation | NavKeys + ViewModel + DI | OB-T001 OB-T003 | None |
| 2. UI Screens | All 5 screens + shared layout | OB-T004 OB-T011 | Phase 1 |
| 3. Testing | ViewModel unit tests | OB-T012 OB-T013 | Phases 12 |
### Critical Path
```
Phase 1 (NavKeys, ViewModel, DI) → Phase 2 (UI Screens) → Phase 3 (Tests)
```
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| *None* | — | — |

View File

@@ -1,198 +0,0 @@
# Feature Specification: Onboarding / Intro Flow
**Feature Branch**: `010-onboarding`
**Created**: 2026-05-09
**Status**: Migrated
**Input**: Brownfield migration — reverse-engineered from existing `feature/intro` module
## Summary
The onboarding intro flow provides a first-run experience that welcomes new users, explains the app's key capabilities (off-grid messaging, mesh networking, location sharing), and guides them through granting the runtime permissions required for Meshtastic to function: Bluetooth, Location, Notifications, and Critical Alerts (Do Not Disturb override). The flow is a linear wizard driven by Navigation 3 with skip/configure actions on each step.
## Goals
1. **Guide first-time users** through a clear, sequential introduction to the app's value proposition and required permissions.
2. **Request runtime permissions** (Bluetooth, Location, Notifications) in context, explaining *why* each is needed before prompting.
3. **Support graceful degradation** — users can skip any permission step and still complete onboarding.
4. **Handle API-level differences** — adapt the permission set dynamically for Android 12+ (BLE permissions) and Android 13+ (POST_NOTIFICATIONS).
5. **Provide a pathway to system Settings** when permissions have been previously denied and must be granted manually.
## Non-Goals
- Persisting "intro completed" state (handled by the caller / app-level DataStore).
- Device pairing or mesh configuration — this is purely the permission onboarding wizard.
- Supporting iOS or Desktop targets — UI screens are currently Android-only (`androidMain`).
- Analytics collection — analytics opt-in is surfaced via `LocalAnalyticsIntroProvider` but not owned by this feature.
## User Scenarios & Testing *(mandatory)*
### User Story 1 — Welcome & Value Proposition (Priority: P1)
A first-time user opens the app and sees a branded welcome screen that highlights three key features: off-grid communication, private mesh networks, and location sharing. The user taps "Get Started" to begin the setup wizard.
**Why this priority**: This is the entry point — without it, no other onboarding step is reachable.
**Independent Test**: Render the `WelcomeScreen`, verify three feature rows are displayed, and verify tapping "Get Started" triggers the `onGetStarted` callback.
**Acceptance Scenarios**:
1. **Given** the app is launched for the first time, **When** the intro flow starts, **Then** the Welcome screen is displayed with "Welcome to" heading, "Meshtastic" title, and three feature rows (connectivity, networks, location).
2. **Given** the Welcome screen is displayed, **When** the user taps "Get Started", **Then** the app navigates to the Bluetooth permission screen.
3. **Given** the Welcome screen is displayed, **Then** no "Skip" button is visible — the only action is "Get Started".
---
### User Story 2 — Bluetooth Permission Grant (Priority: P1)
The user is presented with an explanation of why Bluetooth is needed (device discovery & configuration). They can grant the permission, skip, or navigate to system Settings if the permission was previously denied.
**Why this priority**: Bluetooth is the core transport for Meshtastic; the app is largely non-functional without it.
**Independent Test**: Render `BluetoothScreen` with `showNextButton=false`, verify feature rows and button text, simulate configure tap.
**Acceptance Scenarios**:
1. **Given** the Bluetooth screen is displayed and permissions are NOT granted, **When** the user taps "Configure Bluetooth Permissions", **Then** the system permission dialog is launched.
2. **Given** the Bluetooth screen is displayed and permissions ARE already granted, **When** the screen renders, **Then** the button text changes to "Next" and tapping it navigates to Location.
3. **Given** the Bluetooth screen is displayed, **When** the user taps "Skip", **Then** the app navigates to the Location screen without granting permissions.
4. **Given** the description text contains a "Settings" link, **When** the user taps it, **Then** the app opens the system Application Details settings page.
---
### User Story 3 — Location Permission Grant (Priority: P1)
The user is shown why location access is beneficial (share location, distance measurements, distance filters, mesh map). They can grant, skip, or open Settings.
**Why this priority**: Location is essential for map features and position sharing — a core Meshtastic use case.
**Independent Test**: Render `LocationScreen`, verify four feature rows, simulate permission grant flow.
**Acceptance Scenarios**:
1. **Given** the Location screen is displayed and permissions are NOT granted, **When** the user taps "Configure Location Permissions", **Then** the system permission dialog (fine + coarse) is launched.
2. **Given** the Location screen is displayed and permissions ARE already granted, **Then** the button text is "Next".
3. **Given** the user taps "Skip", **Then** the app navigates to the Notifications screen.
---
### User Story 4 — Notification Permission Grant (Priority: P2)
The user is shown why notifications are valuable (incoming messages, new nodes, low battery alerts). On Android 13+ the system dialog is shown; on older versions the step auto-advances.
**Why this priority**: Notifications enhance the experience but the app is still usable without them.
**Independent Test**: Render `NotificationsScreen`, verify three feature rows, test both API 33+ and pre-33 code paths.
**Acceptance Scenarios**:
1. **Given** Android 13+ and notification permission is NOT granted, **When** the user taps "Configure Notification Permissions", **Then** the POST_NOTIFICATIONS permission dialog is launched.
2. **Given** Android < 13 (no runtime notification permission), **When** the Notifications screen renders, **Then** the button shows "Next" and proceeds to CriticalAlerts.
3. **Given** the user taps "Skip" on the Notifications screen, **Then** `onDone` is invoked and the intro flow ends (no CriticalAlerts step).
4. **Given** notification permission is granted, **When** the user taps "Next", **Then** the app navigates to the CriticalAlerts screen.
---
### User Story 5 — Critical Alerts / DND Configuration (Priority: P3)
The user is informed about critical alerts that can bypass Do Not Disturb. They can configure the notification channel settings or skip.
**Why this priority**: This is an advanced preference; most users can safely skip it.
**Independent Test**: Render `CriticalAlertsScreen`, verify heading and description text, verify "Configure" opens system notification channel settings.
**Acceptance Scenarios**:
1. **Given** the CriticalAlerts screen is displayed, **When** the user taps "Configure Critical Alerts", **Then** the system notification channel settings Intent is launched and `onDone` is called.
2. **Given** the CriticalAlerts screen is displayed, **When** the user taps "Skip", **Then** `onDone` is called and the intro flow ends.
---
### Edge Cases
- What happens when the user rotates the device mid-wizard? → Navigation 3 backstack survives configuration changes via `rememberNavBackStack`.
- What happens on pre-Android-12 devices where BLE permissions don't exist? → `bluetoothPermissions` list is empty; the Bluetooth screen still appears with skip/next.
- What happens if the user presses back? → Navigation 3 backstack handles back navigation to the previous intro step.
- What happens if `createClickableAnnotatedString` fails to find the "Settings" substring? → The annotation is silently skipped (no crash); the text renders as plain text.
## Architecture
### Key Components
| Component | Module / File | Purpose |
|-----------|---------------|---------|
| `IntroViewModel` | `feature/intro/src/commonMain/.../IntroViewModel.kt` | Determines the next navigation key based on current step and permission state |
| `IntroNavKeys` | `feature/intro/src/commonMain/.../IntroNavKeys.kt` | `@Serializable data object` navigation keys: Welcome, Bluetooth, Location, Notifications, CriticalAlerts |
| `FeatureIntroModule` | `feature/intro/src/commonMain/.../di/FeatureIntroModule.kt` | Koin DI module with `@ComponentScan` |
| `AppIntroductionScreen` | `feature/intro/src/androidMain/.../AppIntroductionScreen.kt` | Root composable — hoists permission states and wires nav graph |
| `introNavGraph` | `feature/intro/src/androidMain/.../IntroNavGraph.kt` | Navigation 3 entry provider with per-screen permission handling |
| `PermissionScreenLayout` | `feature/intro/src/androidMain/.../PermissionScreenLayout.kt` | Reusable layout for permission screens (headline, description, features, bottom bar) |
| `IntroBottomBar` | `feature/intro/src/androidMain/.../IntroBottomBar.kt` | Skip / Configure bottom bar used across all intro screens |
| `FeatureUIData` | `feature/intro/src/androidMain/.../FeatureUIData.kt` | Data class for feature row icon + title + subtitle |
| `FeatureRow` | `feature/intro/src/androidMain/.../IntroUiHelpers.kt` | Composable row displaying icon, title, subtitle |
| `createClickableAnnotatedString` | `feature/intro/src/androidMain/.../IntroUiHelpers.kt` | Builds annotated strings with clickable "Settings" links |
| `MeshtasticNavDisplay` | `core/ui/component/` | Shared navigation display wrapper (reused) |
| `MeshtasticIcons` | `core/ui/icon/` | Project icon set (reused) |
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST display a Welcome screen with app branding and three feature highlights on first launch.
- **FR-002**: System MUST present permission screens in fixed order: Bluetooth → Location → Notifications → Critical Alerts.
- **FR-003**: Each permission screen MUST provide a "Skip" button allowing the user to proceed without granting the permission.
- **FR-004**: Each permission screen MUST show a "Configure" button that launches the appropriate system permission dialog.
- **FR-005**: When a permission is already granted, the configure button MUST change to "Next" and skip the system dialog.
- **FR-006**: System MUST adapt the Bluetooth permission set based on API level: `BLUETOOTH_SCAN` + `BLUETOOTH_CONNECT` on Android 12+, empty list on older versions.
- **FR-007**: System MUST only request `POST_NOTIFICATIONS` on Android 13+; on older versions, the Notifications step auto-advances.
- **FR-008**: Each permission screen description MUST contain a clickable "Settings" link that opens the app's system Settings page.
- **FR-009**: The CriticalAlerts "Configure" action MUST open the system notification channel settings with channel ID `"my_alerts"`.
- **FR-010**: Skipping the Notifications screen MUST end the intro flow immediately (no CriticalAlerts step).
### Non-Functional Requirements
- **NFR-001**: All navigation logic (`IntroViewModel.getNextKey`) MUST reside in `commonMain` and be unit-testable without Android framework dependencies.
- **NFR-002**: All user-visible strings MUST use `stringResource(Res.string.*)` from `core:resources` — no hardcoded UI strings.
- **NFR-003**: Icons MUST use `MeshtasticIcons` from `core/ui/icon/`.
- **NFR-004**: The intro flow MUST be scrollable to accommodate small screens and large font sizes.
## Source-Set Impact
| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | 3 files — ViewModel, NavKeys, DI module | Navigation logic and DI wiring (Constitution §I compliant) |
| `androidMain` | 8 files — all UI screens & helpers | Permission APIs require `android.*` imports (Accompanist, Intent, Build.VERSION) |
| `jvmMain` | None | N/A |
## Design Standards Compliance
- [x] New screens reviewed against [design standards](https://raw.githubusercontent.com/meshtastic/design/refs/heads/master/standards/meshtastic_design_standards_latest.md)
- [x] M3 component selection verified (`Scaffold`, `BottomAppBar`, `Button`, `Text` with M3 Typography)
- [ ] Accessibility: TalkBack semantics, touch targets, color-independent info — not explicitly verified
- [x] Typography: `headlineLarge` for headlines, `titleMedium` with `SemiBold` for feature titles, `bodyLarge`/`bodyMedium` for descriptions
## Privacy Assessment
- [x] No PII, location data, or cryptographic keys logged or exposed
- [x] No new network calls that transmit user data
- [x] Proto submodule (`core/proto`) not modified (read-only upstream)
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: All 6 ViewModel navigation tests pass (`IntroViewModelTest`) covering the full step sequence and branching logic.
- **SC-002**: The intro flow renders 5 distinct screens (Welcome, Bluetooth, Location, Notifications, CriticalAlerts) with correct content.
- **SC-003**: Users can complete the entire flow (granting all permissions) or skip every step — both paths invoke `onDone`.
- **SC-004**: On Android 12+ devices, Bluetooth permission dialog is triggered; on older devices, the step is skippable.
- **SC-005**: On Android 13+ devices, notification permission dialog is triggered; on older devices, the step auto-advances.
- **SC-006**: The "Settings" deep link in permission descriptions opens the correct system screen.
## Assumptions
- All business logic and UI composables reside in `commonMain` source set (ViewModel and NavKeys do; UI screens are in `androidMain` — see Gaps).
- String resources added to `core/resources/src/commonMain/composeResources/values/strings.xml`.
- Icons use `MeshtasticIcons` (from `core/ui/icon/`).
- The caller (app module) is responsible for persisting "intro completed" state and conditionally showing the intro flow.
- `LocalAnalyticsIntroProvider` is provided by the app module's composition local, not by this feature.
- The notification channel `"my_alerts"` is pre-created by the app module's notification setup code.

View File

@@ -1,101 +0,0 @@
# Tasks: Onboarding / Intro Flow
**Spec**: [spec.md](./spec.md) | **Plan**: [plan.md](./plan.md)
**Status**: Migrated
**Prefix**: OB-T
---
## Phase 1 — Core Model & Navigation
- [x] **OB-T001**: Define `@Serializable data object` NavKeys (Welcome, Bluetooth, Location, Notifications, CriticalAlerts) implementing `NavKey`
- File: `feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroNavKeys.kt`
- Acceptance: All 5 keys compile and are serializable
- [x] **OB-T002**: Implement `IntroViewModel` with `getNextKey(currentKey, allPermissionsGranted)` navigation logic
- File: `feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroViewModel.kt`
- Acceptance: Welcome→Bluetooth→Location→Notifications→CriticalAlerts (or null) flow works; branching at Notifications based on permission state
- [x] **OB-T003**: Create Koin DI module with `@Module` + `@ComponentScan`
- File: `feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/di/FeatureIntroModule.kt`
- Acceptance: `IntroViewModel` injectable via `@KoinViewModel`
---
## Phase 2 — UI Screens
- [x] **OB-T004**: Implement `FeatureUIData` data class (icon, titleRes, subtitleRes)
- File: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/FeatureUIData.kt`
- Acceptance: Data class holds `ImageVector` + optional `StringResource` title + required subtitle
- [x] **OB-T005**: Implement `FeatureRow` composable and `createClickableAnnotatedString` helper
- File: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroUiHelpers.kt`
- Acceptance: Feature rows render icon + title + subtitle; annotated strings have clickable "Settings" link
- [x] **OB-T006**: Implement `IntroBottomBar` composable (Skip + Configure buttons)
- File: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroBottomBar.kt`
- Acceptance: Bottom bar renders with configurable button text; skip button hidden when `showSkipButton=false`
- [x] **OB-T007**: Implement `PermissionScreenLayout` reusable scaffold
- File: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/PermissionScreenLayout.kt`
- Acceptance: Layout renders headline, annotated description with tap detection, feature rows, and bottom bar
- [x] **OB-T008**: Implement `WelcomeScreen` with 3 feature highlights and "Get Started" CTA
- File: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/WelcomeScreen.kt`
- Acceptance: Renders "Welcome to" + "Meshtastic" headings, 3 feature rows, analytics intro, no skip button
- [x] **OB-T009**: Implement `BluetoothScreen` with API-level-aware permission configuration
- File: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/BluetoothScreen.kt`
- Acceptance: Shows 2 feature rows (discovery, config); adapts button text based on grant state; Settings link works
- [x] **OB-T010**: Implement `LocationScreen` with 4 feature highlights
- File: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/LocationScreen.kt`
- Acceptance: Shows 4 feature rows (share, distance, filters, map); adapts button text based on grant state
- [x] **OB-T011**: Implement `NotificationsScreen` with 3 notification type highlights
- File: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/NotificationsScreen.kt`
- Acceptance: Shows 3 feature rows (messages, nodes, battery); handles Android 13+ POST_NOTIFICATIONS
- [x] **OB-T012**: Implement `CriticalAlertsScreen` with DND override explanation
- File: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/CriticalAlertsScreen.kt`
- Acceptance: Renders headline + description; "Configure" opens notification channel settings; "Skip" invokes onDone
- [x] **OB-T013**: Implement `introNavGraph` entry provider wiring all 5 screens with permission state
- File: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroNavGraph.kt`
- Acceptance: Full navigation flow works with permission granting, skipping, and back navigation
- [x] **OB-T014**: Implement `AppIntroductionScreen` root composable hoisting permission states
- File: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/AppIntroductionScreen.kt`
- Acceptance: Hoists Bluetooth, Location, and Notification permission states; wires `MeshtasticNavDisplay`
---
## Phase 3 — Testing
- [x] **OB-T015**: Write `IntroViewModelTest` covering all navigation transitions
- File: `feature/intro/src/commonTest/kotlin/org/meshtastic/feature/intro/IntroViewModelTest.kt`
- Tests: 6 tests — Welcome→BT, BT→Location, Location→Notifications, Notifications→CriticalAlerts (granted), Notifications→null (not granted), CriticalAlerts→null
- Acceptance: All 6 tests pass via `./gradlew :feature:intro:allTests`
---
## Gaps — Uncompleted Tasks
- [x] **OB-T100**: Extract hardcoded notification channel ID `"my_alerts"` to a shared constant or resource
- File: `feature/intro/src/androidMain/kotlin/org/meshtastic/feature/intro/IntroNavGraph.kt` (line 112)
- Rationale: Hardcoded string is fragile; should reference the same constant used when the channel is created.
- [x] **OB-T101**: Migrate UI screens from `androidMain` to `commonMain` using CMP-compatible permission abstraction
- Created `IntroPermissions` and `IntroSettingsNavigator` abstractions in `commonMain`
- Moved all 8 UI files (screens, nav graph, helpers) to `commonMain`
- Added `AndroidIntroPermissions`/`AndroidIntroSettingsNavigator` adapters in `androidMain` (wrapping Accompanist)
- Added JVM stubs (`JvmIntroDefaults.kt`) with always-granted permissions
- `AppIntroductionScreen` remains in `androidMain` as thin CompositionLocal provider host
- Added CMP `@PreviewLightDark` previews for all 5 screens
- [ ] **[DEFERRED]** **OB-T102**: Add Compose UI tests (screenshot or interaction tests) for all 5 screens — *Deferred: requires Compose UI test infrastructure.*
- Rationale: Only ViewModel logic is unit-tested. No UI rendering or interaction tests exist. Consider `@Preview` screenshot tests or Compose test rule tests.
- [ ] **[DEFERRED]** **OB-T103**: Add accessibility verification — ensure all icons have content descriptions, touch targets ≥ 48dp, and TalkBack announces screen transitions — *Deferred: requires accessibility testing infrastructure.*
- Rationale: Design Standards Compliance (Constitution §V) requires accessibility review. `FeatureRow` icons use `contentDescription` but no formal audit has been done.

View File

@@ -1,143 +0,0 @@
# Implementation Plan: WiFi Provisioning (ESP32 SoftAP)
**Branch**: `011-wifi-provisioning` | **Date**: 2026-06-15 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/011-wifi-provisioning/spec.md`
**Note**: This plan was reverse-engineered from an existing, fully implemented feature (brownfield migration).
## Summary
WiFi Provisioning enables users to configure an ESP32 device's WiFi connection over BLE using the nymea-networkmanager GATT profile. The implementation is a self-contained KMP feature module with a domain layer (nymea protocol codec + GATT client service), a ViewModel state machine, and a Compose Multiplatform UI — all in `commonMain`. The feature depends on `core:ble` for BLE abstraction and `core:testing` for fake BLE implementations.
## Technical Context
**Language/Version**: Kotlin 2.3+ targeting JDK 21
**Primary Dependencies**: Compose Multiplatform, Material 3 Expressive, Koin 4.2+ (K2 Compiler Plugin), kotlinx.serialization, Kermit logging
**Storage**: N/A — no persistence; all state is in-memory scoped to the ViewModel session
**Testing**: KMP `allTests` for `feature:wifi-provision`; `commonTest` with fake BLE implementations from `core:testing`
**Target Platform**: Android, Desktop (JVM), iOS — all via `commonMain`
**Performance Goals**: BLE connect + scan + provision under 30 seconds end-to-end
**Constraints**: All UI in `commonMain`; no `java.*`/`android.*` in common; BLE packets capped at 20 bytes; `safeCatching {}` for error handling
**Scale/Scope**: 11 source files, 5 test files across `feature/wifi-provision`
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Kotlin Multiplatform Core | ✅ PASS | All 11 source files in `commonMain`. No `java.*`/`android.*` imports. BLE abstracted via `core:ble`. |
| II. Zero Lint Tolerance | ✅ PASS | `@Suppress` annotations present only for known Detekt rules (`TooManyFunctions`, `LongMethod`, `MagicNumber` in tests). |
| III. Compose Multiplatform UI | ✅ PASS | CMP composables throughout. Material 3 Expressive components. Navigation 3 `EntryProviderScope` pattern. |
| IV. Privacy First | ✅ PASS | WiFi passwords never logged (only command JSON logged at DEBUG level, passwords inside nymea `"p"` field). No PII stored. |
| V. Design Standards Compliance | ✅ PASS | M3 ListItem, Card, OutlinedTextField, FilledTonalButton, LoadingIndicator. Accessibility: `clickable` with role, content descriptions. |
| VI. Verify Before Push | ✅ PASS | Full verification: `./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests`. |
| VII. Coroutine Safety | ✅ PASS | `safeCatching {}` used in `NymeaWifiService.connect()`, `scanNetworks()`, `provision()`, `fetchConnectionIpAddress()`. Project `CoroutineDispatchers` injected. |
| VIII. Resource Discipline | ✅ PASS | All strings via `stringResource(Res.string.wifi_provision_*)`. Icons via `MeshtasticIcons.*`. |
| IX. Branch & Scope Hygiene | ✅ PASS | Self-contained feature module. No cross-feature changes. |
**Gate Result**: ✅ All principles satisfied
## Project Structure
### Documentation (this feature)
```text
specs/011-wifi-provisioning/
├── spec.md # Feature specification (migrated)
├── plan.md # This file (migrated)
└── tasks.md # Task breakdown (migrated)
```
### Source Code (repository root)
```text
feature/wifi-provision/
├── src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/
│ ├── NymeaBleConstants.kt ← GATT UUIDs, command codes, timeouts
│ ├── WifiProvisionViewModel.kt ← UI state machine (6 phases)
│ ├── di/
│ │ └── FeatureWifiProvisionModule.kt ← Koin DI module
│ ├── domain/
│ │ ├── NymeaPacketCodec.kt ← BLE packet encode/reassemble
│ │ ├── NymeaProtocol.kt ← JSON serialization models
│ │ └── NymeaWifiService.kt ← GATT client: connect, scan, provision
│ ├── model/
│ │ └── WifiNetwork.kt ← WifiNetwork + ProvisionResult models
│ ├── navigation/
│ │ └── WifiProvisionNavigation.kt ← Navigation 3 graph entries
│ └── ui/
│ ├── ProvisionStatusCard.kt ← Inline status card composable
│ ├── WifiProvisionPreviews.kt ← Compose preview definitions
│ └── WifiProvisionScreen.kt ← Main screen with sub-composables
├── src/commonTest/kotlin/org/meshtastic/feature/wifiprovision/
│ ├── DeduplicateBySsidTest.kt ← SSID dedup logic tests
│ ├── WifiProvisionViewModelTest.kt ← ViewModel state machine tests
│ └── domain/
│ ├── NymeaPacketCodecTest.kt ← Packet encode/reassemble tests
│ ├── NymeaProtocolTest.kt ← JSON serialization round-trip tests
│ └── NymeaWifiServiceTest.kt ← GATT client integration tests
```
**Structure Decision**: Self-contained feature module following `feature/{name}` convention. Domain layer lives within the feature (not in `core`) because the nymea protocol is specific to this provisioning flow and not reused elsewhere.
## Module Impact
| Module | Change Type | Files Affected | Risk |
|--------|-------------|----------------|------|
| `feature/wifi-provision` | New | 16 files (11 src + 5 test) | Low — self-contained |
| `core/ble` | None (consumed) | 0 | Low — uses existing interfaces |
| `core/testing` | None (consumed) | 0 | Low — uses existing fakes |
| `core/resources` | Modify | 1 file (strings.xml) | Low — additive string resources |
| `core/ui` | None (consumed) | 0 | Low — reuses existing components |
| `core/navigation` | Modify | 1 file (WifiProvisionRoute) | Low — route registration |
## Integration Points
- **Navigation**: `WifiProvisionRoute.WifiProvisionGraph` and `WifiProvisionRoute.WifiProvision` registered in `wifiProvisionGraph()` extension function on `EntryProviderScope<NavKey>`.
- **DI**: `FeatureWifiProvisionModule` with `@ComponentScan` auto-discovers `@KoinViewModel`-annotated `WifiProvisionViewModel`.
- **BLE**: Injects `BleScanner` and `BleConnectionFactory` from `core:ble` DI graph.
- **Dispatchers**: Injects `CoroutineDispatchers` from `core:di` for testable coroutine contexts.
- **Shared UI**: Reuses `AutoLinkText`, `CopyIconButton` from `core:ui/component/`, `MeshtasticIcons` from `core:ui/icon/`, `AppTheme` from `core:ui/theme/`, `rememberOpenUrl` from `core:ui/util/`.
## Design Constraints
- All UI lives in `commonMain` — not platform-specific
- Strings accessed via `stringResource(Res.string.key)` — never hardcoded
- Icons use `MeshtasticIcons` exclusively (from `core/ui/icon/`)
- Error handling uses `safeCatching {}` not `runCatching {}`
- Dispatchers via `org.meshtastic.core.di.CoroutineDispatchers`
- BLE packets capped at 20 bytes (no MTU negotiation)
- JSON codec uses `kotlinx.serialization` with lenient mode and `ignoreUnknownKeys`
- Navigation uses `dropUnlessResumed` for back stack safety
## Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| BLE connection instability | Medium | Medium | 10s scan timeout, 15s response timeout, typed error handling with user-visible messages |
| JSON protocol version mismatch | Low | High | `ignoreUnknownKeys = true` in JSON codec; lenient parsing |
| MTU > 20 bytes not leveraged | Low | Low | Protocol works at minimum MTU; larger MTU just means fewer packets |
| Password exposure in logs | Low | High | Passwords inside JSON payload only; DEBUG-level logging can be disabled |
## Phase Alignment with Tasks
| Phase | Purpose | Key Tasks | Dependencies |
|-------|---------|-----------|--------------|
| 1. Domain Layer | Protocol models, codec, GATT service | WFP-T001 WFP-T006 | None |
| 2. ViewModel | State machine, actions, error mapping | WFP-T007 WFP-T010 | Phase 1 |
| 3. UI Layer | Screen composables, navigation, DI | WFP-T011 WFP-T016 | Phase 2 |
| 4. Testing | Unit tests for all layers | WFP-T017 WFP-T022 | Phases 13 |
### Critical Path
```
Phase 1 (Domain) → Phase 2 (ViewModel) → Phase 3 (UI) → Phase 4 (Testing)
```
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| *None* | — | — |

View File

@@ -1,210 +0,0 @@
# Feature Specification: WiFi Provisioning (ESP32 SoftAP)
**Feature Branch**: `011-wifi-provisioning`
**Created**: 2026-06-15
**Status**: Migrated
**Input**: Brownfield migration — reverse-engineered from existing `feature/wifi-provision` module
## Summary
WiFi Provisioning enables users to configure an ESP32-based Meshtastic device's WiFi connection over BLE using the nymea-networkmanager GATT profile. The app scans for a nearby nymea device, connects via BLE, retrieves visible WiFi networks, and sends SSID/password credentials to the device. On success, the device's assigned IP address is displayed along with SSH connection details for mPWRD-OS setup. All business logic and UI reside in `commonMain` following KMP conventions.
## Goals
1. **One-tap WiFi setup** — allow users to provision an ESP32 device's WiFi credentials via BLE without needing a serial console or web interface.
2. **Network discovery** — scan and display available WiFi networks from the device's perspective, deduplicated by SSID and sorted by signal strength.
3. **Secure credential transfer** — send SSID + password over BLE using the nymea JSON-over-BLE chunked protocol with `WITH_RESPONSE` writes.
4. **Post-provision guidance** — on successful provisioning, display the device's IP address, default SSH credentials, and a one-tap "Open SSH" action.
5. **Robust error handling** — surface typed errors (ConnectFailed, ScanFailed, ProvisionFailed) with human-readable messages mapped from nymea response codes.
## Non-Goals
- Provisioning via WiFi Direct, USB, or serial — BLE only.
- Managing saved WiFi connections on the device (forget, edit priority).
- WPA3/Enterprise authentication — only PSK and open networks.
- Hidden network provisioning via the UI (domain layer supports `CMD_CONNECT_HIDDEN` but UI does not expose it).
- iOS or Desktop platform-specific BLE implementations — the feature uses `core:ble` abstractions.
## User Scenarios & Testing *(mandatory)*
### User Story 1 — Scan & Connect to BLE Device (Priority: P1)
A user opens the WiFi Provisioning screen. The app automatically scans for a nearby device advertising the nymea wireless GATT service. Once found, the device name is displayed with a confirmation prompt before proceeding.
**Why this priority**: BLE connection is the prerequisite for all other functionality. Without it, no provisioning can occur.
**Independent Test**: Can be fully tested by launching the screen and verifying the BLE scan → DeviceFound → confirmation transition. Delivers confidence that device discovery works.
**Acceptance Scenarios**:
1. **Given** Bluetooth is enabled and a nymea device is advertising, **When** the user opens the WiFi Provisioning screen, **Then** the app scans for up to 10 seconds and transitions to a "Device Found" confirmation showing the device name.
2. **Given** a device is found with no advertised name, **When** the device is discovered, **Then** the MAC address is displayed in place of the name.
3. **Given** Bluetooth is disabled or no device is advertising, **When** the scan timeout (10s) elapses, **Then** a `ConnectFailed` error is displayed and the screen returns to Idle state.
4. **Given** a BLE connection attempt throws an exception, **When** the error is caught, **Then** the error detail message is shown via Snackbar.
---
### User Story 2 — Discover Available WiFi Networks (Priority: P1)
After confirming the discovered device, the user taps "Scan Networks". The app sends a WiFi scan command to the device and displays the list of visible networks with signal strength and lock indicators.
**Why this priority**: Network discovery is the core UX — users need to see and select their WiFi network.
**Independent Test**: Connect to a BLE device, trigger network scan, and verify the network list populates with SSID, signal strength, and protection status.
**Acceptance Scenarios**:
1. **Given** a BLE connection is active, **When** the user taps "Scan Networks", **Then** the app sends CMD_SCAN (4) followed by CMD_GET_NETWORKS (0) and displays the results.
2. **Given** multiple access points with the same SSID, **When** the scan results are received, **Then** duplicates are merged keeping the entry with the strongest signal.
3. **Given** the scan returns an empty list, **When** results are displayed, **Then** a "No networks found" placeholder is shown.
4. **Given** the scan command returns a non-zero error code, **When** the error is received, **Then** a `ScanFailed` error is displayed via Snackbar.
---
### User Story 3 — Provision WiFi Credentials (Priority: P1)
The user selects a network (or types an SSID manually), enters a password, and taps "Apply". The app sends the credentials to the device and reports success or failure with an inline status card.
**Why this priority**: This is the primary action the feature exists to perform.
**Independent Test**: Connect, scan networks, select one, enter password, tap Apply, and verify the ProvisionStatusCard transitions from sending → success/failed.
**Acceptance Scenarios**:
1. **Given** a valid SSID and password are entered, **When** the user taps "Apply", **Then** CMD_CONNECT (1) is sent with the SSID and password and the status card shows "Sending credentials…".
2. **Given** the device responds with success (response code 0) and an IP address, **When** the response is received, **Then** the provision status is set to Success and the IP address is displayed.
3. **Given** the device responds with success but no IP in the payload, **When** the response is received, **Then** a fallback CMD_GET_CONNECTION (5) is sent to retrieve the IP address.
4. **Given** the device responds with a non-zero error code, **When** the response is received, **Then** the provision status is Failed with a mapped error message (e.g., "NetworkManager not available").
5. **Given** the SSID field is blank, **When** the user taps "Apply", **Then** the action is a no-op (button is disabled and provisionWifi guards against blank SSID).
---
### User Story 4 — Post-Provision Success Screen (Priority: P2)
After successful provisioning, the user sees a success screen with the device's IP address, default SSH credentials (username/password), SSH command, and a "Open SSH" button that launches an SSH URI.
**Why this priority**: Provides immediate next-step guidance, especially important for mPWRD-OS devices.
**Independent Test**: Simulate a successful provision and verify the success content displays IP, SSH command, copy buttons, and the Open SSH action.
**Acceptance Scenarios**:
1. **Given** provisioning succeeded with an IP address, **When** the success screen renders, **Then** the IP address, default username, default password, and SSH command are displayed with copy buttons.
2. **Given** provisioning succeeded but the IP address is unavailable, **When** the success screen renders, **Then** a fallback placeholder is shown and the "Open SSH" button is disabled.
3. **Given** the user taps "Done", **When** the action fires, **Then** the BLE connection is closed and the screen navigates back.
---
### User Story 5 — Disconnect & Cleanup (Priority: P3)
The user can cancel at any point. The app disconnects the BLE connection, resets the reassembler buffer, and cancels the service scope. ViewModel cleanup on `onCleared` also cancels the service.
**Why this priority**: Resource cleanup is essential but secondary to the core provisioning flow.
**Independent Test**: Connect to a device, then disconnect and verify the UI state resets to Idle with no leaked resources.
**Acceptance Scenarios**:
1. **Given** an active BLE connection, **When** the user taps "Cancel", **Then** `disconnect()` is called, the BLE connection is closed, and the UI state resets to initial.
2. **Given** the ViewModel is cleared (navigation away), **When** `onCleared` fires, **Then** `service.cancel()` is called to synchronously tear down the scope.
---
### Edge Cases
- What happens when the BLE connection drops mid-scan? The nymea response channel times out (15s) and a `ScanFailed` error is surfaced.
- What happens when the device reports an unknown error code (>7)? The error is mapped to "Unknown error (code N)".
- What happens when `scanNetworks()` is called without an active service? The ViewModel falls back to `connectToDevice()`.
- What happens with very long SSIDs? The UI handles text overflow via Material 3 `ListItem` which truncates with ellipsis.
## Architecture
### Key Components
| Component | Module / File | Purpose |
|-----------|---------------|---------|
| `WifiProvisionViewModel` | `feature/wifi-provision/…/WifiProvisionViewModel.kt` | State machine driving the UI through Idle → ConnectingBle → DeviceFound → LoadingNetworks → Connected → Provisioning phases |
| `NymeaWifiService` | `feature/wifi-provision/…/domain/NymeaWifiService.kt` | GATT client for the nymea-networkmanager profile: connect, scanNetworks, provision, close |
| `NymeaPacketCodec` | `feature/wifi-provision/…/domain/NymeaPacketCodec.kt` | Encode JSON → ≤20-byte BLE packets; Reassembler for inbound notification reassembly |
| `NymeaProtocol` | `feature/wifi-provision/…/domain/NymeaProtocol.kt` | kotlinx.serialization models for nymea JSON commands and responses |
| `NymeaBleConstants` | `feature/wifi-provision/…/NymeaBleConstants.kt` | GATT UUIDs, command codes, response codes, and timeout constants |
| `WifiNetwork` / `ProvisionResult` | `feature/wifi-provision/…/model/WifiNetwork.kt` | Domain models for scan results and provisioning outcomes |
| `WifiProvisionScreen` | `feature/wifi-provision/…/ui/WifiProvisionScreen.kt` | Main Compose screen with Crossfade phase transitions |
| `ProvisionStatusCard` | `feature/wifi-provision/…/ui/ProvisionStatusCard.kt` | Inline status card (sending/success/failed) using Material 3 color semantics |
| `WifiProvisionNavigation` | `feature/wifi-provision/…/navigation/WifiProvisionNavigation.kt` | Navigation 3 entry registration for graph and direct routes |
| `FeatureWifiProvisionModule` | `feature/wifi-provision/…/di/FeatureWifiProvisionModule.kt` | Koin DI module with component scan |
| `BleScanner` / `BleConnectionFactory` | `core/ble/` | Platform-abstracted BLE scanning and connection (reused from core) |
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST scan for BLE devices advertising the nymea wireless service UUID (`e081fec0-…-f7fc`) with a 10-second timeout.
- **FR-002**: System MUST connect to the discovered device via BLE and subscribe to the Commander Response characteristic for notifications.
- **FR-003**: System MUST pause at a "Device Found" confirmation phase showing the device name (or MAC address) before proceeding.
- **FR-004**: System MUST send JSON commands chunked into ≤20-byte BLE packets with a newline (`\n`) terminator using `WITH_RESPONSE` write type.
- **FR-005**: System MUST reassemble inbound BLE notification packets into complete JSON responses using newline-terminated framing.
- **FR-006**: System MUST trigger a WiFi scan on the device (CMD_SCAN=4) and then fetch results (CMD_GET_NETWORKS=0).
- **FR-007**: System MUST deduplicate WiFi networks by SSID, keeping the strongest signal per SSID, and sort descending by signal strength.
- **FR-008**: System MUST send WiFi credentials via CMD_CONNECT (1) for visible networks or CMD_CONNECT_HIDDEN (2) for hidden networks.
- **FR-009**: System MUST map nymea response error codes (07) to human-readable error messages.
- **FR-010**: System MUST attempt a fallback CMD_GET_CONNECTION (5) to retrieve the IP address when the connect response payload lacks one.
- **FR-011**: System MUST display typed errors (`ConnectFailed`, `ScanFailed`, `ProvisionFailed`) via Snackbar with localized messages from string resources.
- **FR-012**: System MUST provide a post-provision success screen showing IP address, default SSH credentials, SSH command, copy buttons, and an "Open SSH" action.
- **FR-013**: System MUST disconnect and cancel the BLE service scope on user-initiated cancel or ViewModel cleanup.
- **FR-014**: System MUST support an optional `address` parameter for targeted BLE device connections (deep-link support).
### Non-Functional Requirements
- **NFR-001**: BLE scan timeout MUST NOT exceed 10 seconds; response timeout MUST NOT exceed 15 seconds.
- **NFR-002**: All UI composables and business logic MUST reside in `commonMain` source set (KMP Constitution §I, §III).
- **NFR-003**: String resources MUST use `stringResource(Res.string.*)` — no hardcoded user-facing text.
- **NFR-004**: Error handling MUST use `safeCatching {}` instead of `runCatching {}` (Constitution §VII).
- **NFR-005**: Password field MUST support visibility toggle and use `PasswordVisualTransformation`.
- **NFR-006**: Haptic feedback MUST fire on successful provisioning via `HapticFeedbackType.LongPress`.
## Source-Set Impact
| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | 11 new files (all feature code) | All business logic, domain, and UI per Constitution §I, §III |
| `commonTest` | 5 new test files | KMP `allTests` for domain + ViewModel |
| `androidMain` | None | No platform-specific code |
| `jvmMain` | None | No JVM-specific code |
## Design Standards Compliance
- [x] New screens reviewed against [design standards](https://raw.githubusercontent.com/meshtastic/design/refs/heads/master/standards/meshtastic_design_standards_latest.md)
- [x] M3 component selection verified (ListItem, Card, OutlinedTextField, FilledTonalButton, LoadingIndicator, Snackbar)
- [x] Accessibility: TalkBack semantics on network rows (`clickable` with `onClickLabel`), password visibility toggle, icon `contentDescription`
- [x] Typography: `headlineSmallEmphasized`, `bodyLargeEmphasized`, `titleLargeEmphasized` for emphasis, M3 scale for hierarchy
## Privacy Assessment
- [x] No PII, location data, or cryptographic keys logged or exposed — WiFi passwords are sent over BLE only, never logged
- [x] No new network calls that transmit user data — all communication is local BLE
- [x] Proto submodule (`core/proto`) not modified (read-only upstream)
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: User can provision a nymea-managed ESP32 device with WiFi credentials via BLE in under 30 seconds (connect + scan + provision).
- **SC-002**: Duplicate SSIDs from multi-AP environments are deduplicated — network list shows at most one entry per SSID.
- **SC-003**: All 7 nymea error codes (17) are mapped to distinct, human-readable error messages.
- **SC-004**: BLE packet codec round-trips correctly: encode → reassemble produces the original JSON for any message size.
- **SC-005**: ViewModel state machine transitions are verified by 15+ test cases covering all 6 phases and error paths.
- **SC-006**: Domain layer (NymeaWifiService, NymeaPacketCodec, NymeaProtocol) has 30+ unit tests with full coverage of command/response flows.
- **SC-007**: Post-provision success screen displays IP address and SSH details within 2 seconds of provisioning completion.
- **SC-008**: BLE resources are cleaned up on disconnect and ViewModel `onCleared` — no leaked coroutine scopes.
## Assumptions
- All business logic and UI composables reside in `commonMain` source set.
- String resources added to `core/resources/src/commonMain/composeResources/values/strings.xml`.
- Icons use `MeshtasticIcons` (from `core/ui/icon/`).
- The nymea-networkmanager BLE profile is available on target ESP32 devices running mPWRD-OS firmware.
- BLE MTU is assumed to be the minimum (20 bytes) — no MTU negotiation is performed.
- Default SSH credentials (username/password) for mPWRD-OS are provided via string resources.
- The `core:ble` module provides working `BleScanner` and `BleConnectionFactory` abstractions with fake implementations for testing.

View File

@@ -1,217 +0,0 @@
# Tasks: WiFi Provisioning (ESP32 SoftAP)
**Spec**: [spec.md](./spec.md) | **Plan**: [plan.md](./plan.md)
**Status**: Migrated — all tasks reflect implemented code
**Prefix**: WFP-T
---
## Phase 1 — Domain Layer
### WFP-T001: Define BLE constants and protocol parameters
- [x] Create `NymeaBleConstants.kt` with GATT UUIDs (Wireless Service, Commander, Response, Connection Status, Network Service)
- [x] Define command codes: `CMD_GET_NETWORKS` (0), `CMD_CONNECT` (1), `CMD_CONNECT_HIDDEN` (2), `CMD_SCAN` (4), `CMD_GET_CONNECTION` (5)
- [x] Define response error codes (07) for success, invalid command, invalid parameter, etc.
- [x] Define timing constants: `SCAN_TIMEOUT` (10s), `RESPONSE_TIMEOUT` (15s), `CONNECTION_INFO_TIMEOUT` (2s), `SUBSCRIPTION_SETTLE` (300ms)
- **File**: `feature/wifi-provision/src/commonMain/…/NymeaBleConstants.kt`
### WFP-T002: Implement BLE packet codec (encode + reassemble)
- [x] Implement `NymeaPacketCodec.encode()` — split JSON + `\n` terminator into ≤20-byte packets
- [x] Implement `NymeaPacketCodec.Reassembler` — stateful reassembler that buffers partial notifications and emits complete JSON on `\n`
- [x] Implement `Reassembler.reset()` for cleanup
- **File**: `feature/wifi-provision/src/commonMain/…/domain/NymeaPacketCodec.kt`
### WFP-T003: Define nymea JSON protocol models
- [x] Create `NymeaSimpleCommand` (`@Serializable`, `@SerialName("c")`) for parameter-less commands
- [x] Create `NymeaConnectParams` with `ssid` (`"e"`) and `password` (`"p"`) fields
- [x] Create `NymeaConnectCommand` with nested `NymeaConnectParams`
- [x] Create `NymeaResponse` with `command`, `responseCode`, optional `connectionInfo`
- [x] Create `NymeaNetworkEntry` with `ssid`, `bssid`, `signalStrength`, `protection`
- [x] Create `NymeaNetworksResponse` with network list payload
- [x] Create `NymeaConnectionInfo` with `ssid`, `bssid`, `signalStrength`, `protection`, `ipAddress`
- [x] Configure shared `NymeaJson` codec with `ignoreUnknownKeys`, `isLenient`
- **File**: `feature/wifi-provision/src/commonMain/…/domain/NymeaProtocol.kt`
### WFP-T004: Define domain models
- [x] Create `WifiNetwork` data class with `ssid`, `bssid`, `signalStrength`, `isProtected`
- [x] Create `ProvisionResult` sealed interface with `Success` (optional `ipAddress`) and `Failure` (errorCode, message)
- **File**: `feature/wifi-provision/src/commonMain/…/model/WifiNetwork.kt`
### WFP-T005: Implement NymeaWifiService GATT client
- [x] Implement `connect()` — scan for nymea device, BLE connect, discover wireless service, subscribe to response characteristic
- [x] Implement `scanNetworks()` — send CMD_SCAN, wait for ack, send CMD_GET_NETWORKS, parse response into `List<WifiNetwork>`
- [x] Implement `provision()` — send CMD_CONNECT or CMD_CONNECT_HIDDEN with SSID/password, parse response into `ProvisionResult`
- [x] Implement `fetchConnectionIpAddress()` — fallback CMD_GET_CONNECTION with short timeout
- [x] Implement `sendCommand()` — encode JSON, write packets with `WITH_RESPONSE`
- [x] Implement `waitForResponse()` — await on response channel with timeout
- [x] Implement `nymeaErrorMessage()` — map error codes 17 to human-readable strings
- [x] Implement `close()` (suspend) and `cancel()` (synchronous) for resource cleanup
- **File**: `feature/wifi-provision/src/commonMain/…/domain/NymeaWifiService.kt`
### WFP-T006: Define typed error categories
- [x] Create `WifiProvisionError` sealed interface with `detail: String`
- [x] Implement `ConnectFailed`, `ScanFailed`, `ProvisionFailed` subtypes
- **File**: `feature/wifi-provision/src/commonMain/…/WifiProvisionViewModel.kt` (top-level declarations)
---
## Phase 2 — ViewModel
### WFP-T007: Define UI state model
- [x] Create `WifiProvisionUiState` data class with `phase`, `networks`, `error`, `deviceName`, `ipAddress`, `provisionStatus`
- [x] Define `Phase` enum: `Idle`, `ConnectingBle`, `DeviceFound`, `LoadingNetworks`, `Connected`, `Provisioning`
- [x] Define `ProvisionStatus` enum: `Idle`, `Success`, `Failed`
- **File**: `feature/wifi-provision/src/commonMain/…/WifiProvisionViewModel.kt`
### WFP-T008: Implement WifiProvisionViewModel
- [x] Create `@KoinViewModel` class with `BleScanner`, `BleConnectionFactory`, `CoroutineDispatchers` injection
- [x] Expose `uiState: StateFlow<WifiProvisionUiState>` via `MutableStateFlow`
- [x] Implement lazy `NymeaWifiService` creation per session
- **File**: `feature/wifi-provision/src/commonMain/…/WifiProvisionViewModel.kt`
### WFP-T009: Implement ViewModel actions
- [x] Implement `connectToDevice(address?)` — scan → connect → DeviceFound, with ConnectFailed error path
- [x] Implement `scanNetworks()` — auto-reconnect if no service, delegate to `loadNetworks()`
- [x] Implement `provisionWifi(ssid, password)` — guard blank SSID, send credentials, map Success/Failure result
- [x] Implement `disconnect()` — close service, reset state
- [x] Implement `onCleared()` — synchronous `service.cancel()`
- **File**: `feature/wifi-provision/src/commonMain/…/WifiProvisionViewModel.kt`
### WFP-T010: Implement SSID deduplication
- [x] Implement `deduplicateBySsid()` — group by SSID, keep strongest signal, sort descending
- [x] Mark as `internal` companion function for testability
- **File**: `feature/wifi-provision/src/commonMain/…/WifiProvisionViewModel.kt`
---
## Phase 3 — UI Layer
### WFP-T011: Implement main WiFi Provisioning screen
- [x] Create `WifiProvisionScreen` composable with `Scaffold`, `CenterAlignedTopAppBar`, `SnackbarHost`
- [x] Implement `Crossfade` transitions between `ScreenKey` states (ConnectingBle, DeviceFound, LoadingNetworks, Connected)
- [x] Add `LinearProgressIndicator` for loading phases
- [x] Wire `LaunchedEffect` for auto-connect on screen entry and error-to-Snackbar display
- **File**: `feature/wifi-provision/src/commonMain/…/ui/WifiProvisionScreen.kt`
### WFP-T012: Implement phase sub-composables
- [x] `ScanningBleContent` — centered `LoadingIndicator` with scanning message
- [x] `DeviceFoundContent` — Bluetooth icon, device name, "Scan Networks" / "Cancel" buttons
- [x] `ScanningNetworksContent` — centered `LoadingIndicator` with WiFi scanning message
- [x] `ConnectedContent` — scan button, network list (LazyColumn in Card), SSID/password fields, Apply/Cancel buttons
- [x] `ProvisionSuccessContent` — check icon, IP address, SSH credentials card, Open SSH button, Done button
- [x] `NetworkRow` — ListItem with WiFi icon, signal strength, lock indicator, selection highlight
- **File**: `feature/wifi-provision/src/commonMain/…/ui/WifiProvisionScreen.kt`
### WFP-T013: Implement ProvisionStatusCard
- [x] Create `ProvisionStatusCard` composable with M3 color semantics (secondary=sending, primary=success, error=failed)
- [x] Implement `StatusIcon` with `LoadingIndicator` / `Success` / `Error` icons
- [x] Implement `statusText` with localized strings
- **File**: `feature/wifi-provision/src/commonMain/…/ui/ProvisionStatusCard.kt`
### WFP-T014: Implement mPWRD disclaimer banner
- [x] Create `MpwrdDisclaimerBanner` with mPWRD logo image and `AutoLinkText` disclaimer
- **File**: `feature/wifi-provision/src/commonMain/…/ui/WifiProvisionScreen.kt`
### WFP-T015: Add Compose previews
- [x] Create preview composables for all phases: scanning BLE, device found (with/without name), scanning networks, connected (with networks, empty, scanning, provisioning, success, failed), edge cases (long SSID, many networks), standalone components
- **File**: `feature/wifi-provision/src/commonMain/…/ui/WifiProvisionPreviews.kt`
### WFP-T016: Wire navigation and DI
- [x] Create `wifiProvisionGraph()` extension function registering `WifiProvisionGraph` and `WifiProvision` nav entries
- [x] Create `FeatureWifiProvisionModule` with `@Module` and `@ComponentScan`
- **Files**: `feature/wifi-provision/src/commonMain/…/navigation/WifiProvisionNavigation.kt`, `…/di/FeatureWifiProvisionModule.kt`
---
## Phase 4 — Testing
### WFP-T017: Test NymeaPacketCodec
- [x] Test encode appends newline terminator
- [x] Test short message fits in single packet
- [x] Test long message splits across multiple packets
- [x] Test boundary conditions (exactly fills packet, one byte over)
- [x] Test empty string encoding
- [x] Test custom maxPacketSize
- [x] Test Reassembler single feed, buffered partial, multi-chunk completion
- [x] Test Reassembler sequential messages and reset
- [x] Test encode → reassemble round-trip at default and small packet sizes
- **File**: `feature/wifi-provision/src/commonTest/…/domain/NymeaPacketCodecTest.kt` — 12 test cases
### WFP-T018: Test NymeaProtocol serialization
- [x] Test NymeaSimpleCommand compact JSON serialization and round-trip
- [x] Test NymeaConnectCommand with nested params, empty password, round-trip
- [x] Test NymeaResponse deserialization for success, error codes, connection info payload, unknown keys
- [x] Test NymeaNetworksResponse with network list, empty list, default field values
- **File**: `feature/wifi-provision/src/commonTest/…/domain/NymeaProtocolTest.kt` — 11 test cases
### WFP-T019: Test NymeaWifiService
- [x] Test connect succeeds and returns device name / address
- [x] Test connect fails on BLE connection failure and exception
- [x] Test scanNetworks returns parsed network list and empty list
- [x] Test scanNetworks fails on error response code
- [x] Test scanNetworks sends correct BLE commands with WITH_RESPONSE write type
- [x] Test provision returns Success on code 0 with IP, and Failure on non-zero codes
- [x] Test provision falls back to GetConnection for IP when payload is empty
- [x] Test provision sends CMD_CONNECT vs CMD_CONNECT_HIDDEN
- [x] Test provision maps all 7 known error codes
- [x] Test close disconnects BLE
- **File**: `feature/wifi-provision/src/commonTest/…/domain/NymeaWifiServiceTest.kt` — 14 test cases
### WFP-T020: Test SSID deduplication
- [x] Test empty list returns empty
- [x] Test single network unchanged
- [x] Test duplicate SSIDs keep strongest signal
- [x] Test mixed duplicates and unique networks
- [x] Test result sorted by signal strength descending
- [x] Test preserves isProtected from strongest entry
- **File**: `feature/wifi-provision/src/commonTest/…/DeduplicateBySsidTest.kt` — 6 test cases
### WFP-T021: Test WifiProvisionViewModel
- [x] Test initial state is Idle with empty data
- [x] Test connectToDevice transitions: ConnectingBle → DeviceFound on success
- [x] Test connectToDevice uses address when name is null
- [x] Test connectToDevice sets ConnectFailed error on failure and exception
- [x] Test scanNetworks transitions: LoadingNetworks → Connected with deduplicated results
- [x] Test scanNetworks reconnects if no service exists
- [x] Test provisionWifi transitions: Provisioning → Connected with Success/Failed status
- [x] Test provisionWifi ignores blank SSID and no-ops when service is null
- [x] Test disconnect resets state and calls BLE disconnect
- **File**: `feature/wifi-provision/src/commonTest/…/WifiProvisionViewModelTest.kt` — 13 test cases
### WFP-T022: Add string resources
- [x] Add all `wifi_provision_*` string resources to `core/resources/src/commonMain/composeResources/values/strings.xml`
- [x] Run `sort-strings.py` to maintain alphabetical order
---
## Identified Gaps (not yet implemented)
### WFP-T023: Expose hidden network provisioning in UI
- [x] Add a "Hidden Network" toggle or option in `ConnectedContent` that sets `hidden = true` when calling `provisionWifi`
- [ ] **[DEFERRED]** Domain layer already supports `CMD_CONNECT_HIDDEN` (2) — only UI wiring needed — *Deferred: already implemented in WFP-T023; this sub-task is redundant.*
- **Priority**: Low — niche use case
### WFP-T024: Add retry mechanism for BLE scan timeout
- [x] When BLE scan times out (10s), offer a "Retry" button instead of requiring the user to navigate back and re-enter
- [ ] **[DEFERRED]** Consider exponential backoff or a manual retry count limit — *Deferred: enhancement — current retry UX is sufficient for v1.*
- **Priority**: Medium — improves UX for unreliable BLE environments
### WFP-T025: Add Compose UI tests
- [ ] **[DEFERRED]** Add `@Test` composable tests for `WifiProvisionScreen` phase transitions (ConnectingBle → DeviceFound → Connected) — *Deferred: requires Compose UI test infrastructure.*
- [ ] **[DEFERRED]** Add interaction tests for network selection, SSID/password input, Apply button enable/disable — *Deferred: requires Compose UI test infrastructure.*
- [ ] **[DEFERRED]** Add snapshot or screenshot tests for `ProvisionStatusCard` states — *Deferred: requires Compose UI test infrastructure.*
- **Priority**: Medium — domain and ViewModel well-tested, but UI layer lacks automated verification
---
## Summary
| Category | Total | Completed | Gaps |
|----------|-------|-----------|------|
| Domain Layer | 6 | 6 ✅ | 0 |
| ViewModel | 4 | 4 ✅ | 0 |
| UI Layer | 6 | 6 ✅ | 0 |
| Testing | 6 | 6 ✅ | 0 |
| Gaps | 3 | 0 | 3 ⚠️ |
| **Total** | **25** | **22** | **3** |

View File

@@ -1,157 +0,0 @@
# Implementation Plan: Core Data Layer
**Branch**: `012-core-data` | **Date**: 2026-07-27 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/012-core-data/spec.md`
**Status**: Migrated — all implementation complete, plan reverse-engineered from existing code.
## Summary
The Core Data Layer provides all concrete implementations for the repository and manager interfaces that feature modules consume. It orchestrates mesh connection lifecycle, packet processing, node management, session tracking, radio configuration, MQTT, firmware data, and XModem transfers. The module is pure `commonMain` Kotlin with Koin DI, depending downward on `core/database`, `core/network`, `core/ble`, `core/model`, and `core/repository`.
## Technical Context
**Language/Version**: Kotlin 2.3+ targeting JDK 21
**Primary Dependencies**: Koin 4.2+ (K2 Compiler Plugin), kotlinx.coroutines, kotlinx.atomicfu, kotlinx-collections-immutable, Okio, Kermit logging
**Storage**: Room KMP via `DatabaseProvider.withDb()` delegation; DataStore KMP for preferences
**Testing**: KMP `allTests` — 19 test files, ~1,700 LOC; Turbine for Flow testing, Mokkery for mocking
**Target Platform**: Android, Desktop (JVM), iOS — all via `commonMain`
**Constraints**: No `java.*`/`android.*` imports in commonMain; all dispatchers via `CoroutineDispatchers`; `safeCatching {}` over `runCatching {}`
**Scale/Scope**: 40 commonMain files (~10,500 LOC), 0 androidMain files, 19 test files (~1,700 LOC)
## Constitution Check
*GATE: All checks pass — existing production code reviewed against Constitution v1.2.2.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Kotlin Multiplatform Core | ✅ PASS | All 40 source files in `commonMain`. Zero platform-specific code. |
| II. Zero Lint Tolerance | ✅ PASS | `detekt-baseline.xml` present. Suppressions limited to `TooManyFunctions`, `LongParameterList`. |
| III. Compose Multiplatform UI | N/A | No UI code in this module. |
| IV. Privacy First | ✅ PASS | Node identifiers anonymized via `anonymize()`. Message content never logged. |
| V. Design Standards Compliance | N/A | No UI code. |
| VI. Verify Before Push | ✅ PASS | 19 test files covering all critical managers and repositories. |
| VII. Coroutine Safety | ✅ PASS | Named dispatchers throughout. `safeCatching {}` used in MQTT and packet handlers. `atomicfu` for session state. |
| VIII. Resource Discipline | N/A | No string/icon resources in data layer. |
| IX. Branch & Scope Hygiene | ✅ PASS | Clean module boundaries. All implementations scoped to `org.meshtastic.core.data`. |
**Gate Result**: ✅ All applicable principles satisfied.
## Project Structure
```
core/data/src/
├── commonMain/kotlin/org/meshtastic/core/data/
│ ├── di/
│ │ └── CoreDataModule.kt # Koin module with @ComponentScan
│ ├── datasource/
│ │ ├── NodeInfoReadDataSource.kt # Interface for reading node info
│ │ ├── NodeInfoWriteDataSource.kt # Interface for writing node info
│ │ ├── SwitchingNodeInfoReadDataSource.kt # Switches between real/mock sources
│ │ ├── SwitchingNodeInfoWriteDataSource.kt # Switches between real/mock sources
│ │ ├── FirmwareReleaseJsonDataSource.kt # JSON parsing for firmware releases
│ │ ├── FirmwareReleaseLocalDataSource.kt # Room-backed firmware cache
│ │ ├── DeviceHardwareJsonDataSource.kt # JSON parsing for hardware catalog
│ │ ├── DeviceHardwareLocalDataSource.kt # Room-backed hardware cache
│ │ └── BootloaderOtaQuirksJsonDataSource.kt # OTA bootloader quirks data
│ ├── repository/
│ │ ├── NodeRepositoryImpl.kt # Node CRUD, sort, filter, flows
│ │ ├── PacketRepositoryImpl.kt # Message/packet persistence, paging
│ │ ├── RadioConfigRepositoryImpl.kt # Radio config state flows
│ │ ├── FirmwareReleaseRepositoryImpl.kt # Firmware release catalog
│ │ ├── DeviceHardwareRepositoryImpl.kt # Hardware catalog
│ │ ├── QuickChatActionRepositoryImpl.kt # Quick chat shortcuts
│ │ ├── MeshLogRepositoryImpl.kt # Debug mesh log persistence
│ │ └── TracerouteSnapshotRepositoryImpl.kt # Traceroute result persistence
│ └── manager/
│ ├── MeshConnectionManagerImpl.kt # Connection lifecycle (436 LOC)
│ ├── FromRadioPacketHandlerImpl.kt # Top-level packet dispatcher
│ ├── PacketHandlerImpl.kt # Per-portnum routing
│ ├── MeshMessageProcessorImpl.kt # Message persistence + notifications
│ ├── TelemetryPacketHandlerImpl.kt # Telemetry metric updates
│ ├── AdminPacketHandlerImpl.kt # Admin response processing
│ ├── StoreForwardPacketHandlerImpl.kt # Store-and-forward handling
│ ├── NeighborInfoHandlerImpl.kt # Neighbor info updates
│ ├── MeshDataHandlerImpl.kt # Generic data packet handling
│ ├── NodeManagerImpl.kt # Node cache + Room sync
│ ├── SessionManagerImpl.kt # Per-node remote-admin sessions
│ ├── CommandSenderImpl.kt # Admin/data packet construction
│ ├── MeshRouterImpl.kt # Service action routing
│ ├── MeshActionHandlerImpl.kt # Service action handler
│ ├── MeshConfigHandlerImpl.kt # Config response processing
│ ├── MeshConfigFlowManagerImpl.kt # Request/response correlation
│ ├── MqttManagerImpl.kt # MQTT lifecycle
│ ├── XModemManagerImpl.kt # XModem file transfer
│ ├── HistoryManagerImpl.kt # Sent-packet history
│ ├── MessageFilterImpl.kt # Mute/ignore filtering
│ └── DataLayerHeartbeatSender.kt # Periodic heartbeat
└── commonTest/kotlin/org/meshtastic/core/data/
├── repository/
│ ├── CommonMeshLogRepositoryTest.kt
│ ├── CommonPacketRepositoryTest.kt
│ └── CommonNodeRepositoryTest.kt
└── manager/
├── MeshConnectionManagerImplTest.kt
├── SessionManagerImplTest.kt
├── NodeManagerImplTest.kt
├── PacketHandlerImplTest.kt
├── FromRadioPacketHandlerImplTest.kt
├── MeshMessageProcessorImplTest.kt
├── TelemetryPacketHandlerImplTest.kt
├── AdminPacketHandlerImplTest.kt
├── StoreForwardPacketHandlerImplTest.kt
├── MeshDataHandlerTest.kt
├── MeshConfigHandlerImplTest.kt
├── MeshConfigFlowManagerImplTest.kt
├── MeshActionHandlerImplTest.kt
├── MessageFilterImplTest.kt
├── HistoryManagerImplTest.kt
└── XModemManagerImplTest.kt
```
## Implementation Phases
### Phase 1 — DI & Data Sources (Complete)
Core infrastructure: Koin module, data source interfaces, switching data sources for real/mock node info, JSON data sources for firmware and hardware catalogs.
### Phase 2 — Repository Implementations (Complete)
All 8 repository implementations providing CRUD, reactive flows, pagination, and caching over the Room database and DataStore.
### Phase 3 — Manager Implementations: Connection & Packet Pipeline (Complete)
The critical path: `MeshConnectionManagerImpl` (436 LOC connection lifecycle), `FromRadioPacketHandlerImpl``PacketHandlerImpl` → per-portnum handlers, `NodeManagerImpl`, `CommandSenderImpl`.
### Phase 4 — Manager Implementations: Support Services (Complete)
Supporting managers: `SessionManagerImpl` (atomicfu-backed per-node sessions), `MeshConfigFlowManagerImpl` (request/response correlation), `MqttManagerImpl`, `XModemManagerImpl`, `HistoryManagerImpl`, `MessageFilterImpl`, `DataLayerHeartbeatSender`.
## Technical Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| DI strategy | Koin `@ComponentScan` | Auto-discovers all `@Single` implementations without manual registration |
| Session state | `atomicfu` + `PersistentMap` | Lock-free, thread-safe per-node session storage |
| Database access | `withDb()` indirection | Tolerates database switching; retries on connection-pool-closed |
| Packet routing | When-expression on `PortNum` | Simple, exhaustive, easy to extend for new port numbers |
| Config correlation | `MeshConfigFlowManager` | Suspending request/response pairs with timeout for admin commands |
| Node cache | In-memory `StateFlow<List<Node>>` + Room | Fast UI reads from memory; persistence survives process death |
| MQTT lifecycle | `MqttManagerImpl` wrapping `MqttClient` | Decouples MQTT client lifecycle from connection manager |
| Heartbeat | `DataLayerHeartbeatSender` | Periodic keep-alive to prevent radio sleep during active sessions |
| Time abstraction | Injected `kotlin.time.Clock` | Enables deterministic testing of TTL and timestamp logic |
| Error handling | `safeCatching {}` in handlers | Prevents a single packet processing failure from crashing the pipeline |
| Switching data sources | `SwitchingNodeInfo*DataSource` | Enables mock mode for screenshots and testing without a real radio |
| Firmware/hardware catalogs | JSON + Room cache | Network-fetched catalogs cached locally for offline access |
## Gaps Identified
| Gap | Severity | Recommendation |
|-----|----------|----------------|
| `MqttManagerImpl` has no unit test | ⚠️ Medium | Add tests for connect/subscribe/publish/disconnect lifecycle |
| `MeshRouterImpl` has no unit test | ⚠️ Medium | Add tests for service action routing (send, traceroute, position request) |
| `TracerouteHandlerImpl` has no unit test | ⚠️ Low | Add tests for traceroute snapshot construction |
| `DataLayerHeartbeatSender` has no unit test | ⚠️ Low | Add tests for heartbeat interval and cancellation |
| `NeighborInfoHandlerImpl` has no unit test | ⚠️ Low | Add tests for neighbor info upsert logic |
| No integration test for full packet pipeline | ⚠️ Medium | Add end-to-end test: raw `FromRadio` bytes → persisted `Packet` entity |
| `MeshConnectionManagerImpl` uses `runCatching` in one place | ⚠️ Low | Should use `safeCatching` per Constitution §VII |

View File

@@ -1,215 +0,0 @@
# Feature Specification: Core Data Layer
**Feature Branch**: `012-core-data`
**Created**: 2026-07-27
**Status**: Migrated
**Input**: Brownfield migration — reverse-engineered from existing `core/data` module
## Summary
The Core Data Layer is the central orchestration hub of Meshtastic-Android. It provides concrete implementations of all repository and manager interfaces defined in `core/repository`, bridging the mesh radio transport layer (`core/network`) with the persistence layer (`core/database`) and the feature modules. The module handles mesh connection lifecycle, packet routing and processing, node management, session tracking, radio configuration, MQTT integration, firmware release data, XModem file transfer, and telemetry. All implementations reside in `commonMain` with Koin DI component scan.
## Goals
1. **Centralized data orchestration** — provide a single module with all `*Impl` classes that feature modules depend on via interface-only injection.
2. **Mesh connection lifecycle** — manage the full connect → handshake → config complete → operational → disconnect lifecycle with status notifications.
3. **Packet processing pipeline** — decode, route, and persist all `FromRadio` packets through a layered handler chain (packet → message → telemetry → admin → store-forward → neighbor).
4. **Node management** — maintain an in-memory node database backed by Room, with identity, position, telemetry, and hardware metadata.
5. **Session management** — track per-node remote-admin sessions with TTL, passkey rotation, and automatic expiry aligned with firmware's 300s TTL.
## Non-Goals
- Transport-level concerns (BLE, TCP, Serial) — handled by `core/network`.
- Database schema definitions or DAO interfaces — handled by `core/database`.
- Domain model definitions — handled by `core/model`.
- UI or ViewModel logic — handled by `feature/*` modules.
- Proto message definitions — handled by `core/proto` (read-only upstream).
## User Scenarios & Testing *(mandatory)*
### User Story 1 — Mesh Connection Lifecycle (Priority: P1)
When the app connects to a Meshtastic radio, the data layer orchestrates the full handshake: device identification, configuration exchange, node DB loading, and transition to `CONNECTED` state. The connection manager coordinates with the radio interface, node manager, notifications, and analytics.
**Why this priority**: Every feature depends on an active mesh connection. Without a successful handshake, no data flows.
**Independent Test**: Can be validated by simulating a radio connection and verifying state transitions through Disconnected → DeviceSleep → Connected.
**Acceptance Scenarios**:
1. **Given** a radio transport is available, **When** the connection manager starts, **Then** it transitions through `Disconnected → DeviceSleep → Connected` as handshake packets arrive.
2. **Given** a config-complete packet is received, **When** the handshake finishes, **Then** the node DB is saved, MQTT is started if configured, and connection state is set to `Connected`.
3. **Given** the radio disconnects unexpectedly, **When** the transport reports disconnection, **Then** the connection manager transitions to `DeviceSleep` and analytics are reported.
4. **Given** multiple connect/disconnect cycles occur, **When** each cycle completes, **Then** no coroutine leaks or state corruption occurs.
---
### User Story 2 — Packet Processing Pipeline (Priority: P1)
When the radio receives a mesh packet, it flows through a pipeline of handlers: `FromRadioPacketHandler` dispatches to `PacketHandler`, which routes to `MeshMessageProcessor`, `TelemetryPacketHandler`, `AdminPacketHandler`, `StoreForwardPacketHandler`, and `NeighborInfoHandler` based on port number.
**Why this priority**: All mesh data (messages, telemetry, admin responses, traceroutes) enters the app through this pipeline.
**Independent Test**: Feed a `FromRadio` proto to `handleFromRadio()` and verify the correct handler processes it and persists the result.
**Acceptance Scenarios**:
1. **Given** a `FromRadio` packet with a `MESH_PACKET` variant, **When** processed by `FromRadioPacketHandler`, **Then** it is decoded and dispatched to `PacketHandler`.
2. **Given** a packet with `PortNum.TEXT_MESSAGE_APP`, **When** routed by `PacketHandler`, **Then** `MeshMessageProcessor` persists it as a `Packet` entity and triggers notifications.
3. **Given** a packet with `PortNum.TELEMETRY_APP`, **When** routed, **Then** `TelemetryPacketHandler` updates the node's device/environment/power metrics.
4. **Given** a packet with `PortNum.ADMIN_APP`, **When** routed, **Then** `AdminPacketHandler` processes admin responses and updates radio configuration.
5. **Given** a store-and-forward history packet, **When** processed, **Then** `StoreForwardPacketHandler` persists it without duplicate notification.
---
### User Story 3 — Node Management (Priority: P1)
The node manager maintains a reactive in-memory cache of all mesh nodes, synchronized with Room persistence. It handles node discovery, identity updates, position changes, and last-heard timestamps.
**Why this priority**: Node data is displayed across 6+ feature screens. Stale or missing node data degrades the entire UX.
**Independent Test**: Simulate node info packets and verify the node cache updates and Room persists correctly.
**Acceptance Scenarios**:
1. **Given** a `NodeInfo` packet is received, **When** `NodeManager` processes it, **Then** the node is upserted in both the in-memory cache and Room database.
2. **Given** a node's user identity changes (new long_name/short_name), **When** the update is processed, **Then** the cached `Node` reflects the new identity immediately.
3. **Given** a node has not been heard for >15 minutes, **When** `isOnline` is evaluated, **Then** it returns `false`.
4. **Given** `loadCachedNodeDB()` is called on startup, **Then** all persisted nodes are loaded into the in-memory cache.
---
### User Story 4 — Radio Configuration Management (Priority: P2)
The radio config repository manages the local copy of the connected device's configuration (LoRa, device, display, network, Bluetooth, position, power, security). Configuration changes are sent via admin packets and confirmed via response packets.
**Why this priority**: Settings screens depend on accurate config state. Stale config leads to user confusion.
**Independent Test**: Simulate config response packets and verify the repository state updates.
**Acceptance Scenarios**:
1. **Given** a `Config` admin response is received, **When** `MeshConfigHandler` processes it, **Then** the corresponding config flow in `RadioConfigRepository` is updated.
2. **Given** the user changes a config value, **When** the change is sent via `CommandSender`, **Then** the admin packet is constructed and sent to the radio.
3. **Given** a `MeshConfigFlowManager` is active, **When** config responses arrive, **Then** they are correlated with pending requests and the flow completes.
---
### User Story 5 — Session Management for Remote Admin (Priority: P2)
The session manager tracks per-node remote-admin sessions, including passkey storage, TTL tracking, and automatic expiry detection. This enables the remote admin feature to maintain sessions across navigation without re-authenticating.
**Why this priority**: Remote admin is a power-user feature. Session management prevents passkey conflicts when administering multiple nodes.
**Independent Test**: Create sessions for multiple nodes and verify TTL expiry and passkey rotation.
**Acceptance Scenarios**:
1. **Given** a remote-admin session is established with node A, **When** the passkey response arrives, **Then** `SessionManager` stores the passkey mapped to node A's num.
2. **Given** sessions exist for nodes A and B, **When** node B's session is accessed, **Then** node A's passkey is not overwritten (per-node isolation).
3. **Given** a session is 240+ seconds old, **When** `statusFlow(nodeNum)` is observed, **Then** it emits `Expired`.
4. **Given** a session receives a refreshed passkey, **When** the refresh is processed, **Then** the TTL resets to 300s.
---
### Edge Cases
- What happens when `FromRadio` contains an unknown `payloadVariant`? It is logged and ignored.
- What happens when the packet handler receives a packet with `from == 0`? It is treated as from the local node.
- What happens when MQTT reconnects mid-session? `MqttManagerImpl` re-subscribes to all configured topics.
- What happens when `withDb()` is called during a database switch? The connection-pool-closed exception is caught and retried once with the new DB instance.
## Architecture
### Key Components
| Component | File | Purpose |
|-----------|------|---------|
| `MeshConnectionManagerImpl` | `manager/MeshConnectionManagerImpl.kt` | Full mesh connection lifecycle: handshake, config exchange, state machine |
| `FromRadioPacketHandlerImpl` | `manager/FromRadioPacketHandlerImpl.kt` | Top-level `FromRadio` dispatcher — routes to packet, config, nodeinfo handlers |
| `PacketHandlerImpl` | `manager/PacketHandlerImpl.kt` | Per-portnum routing: text → message processor, telemetry → telemetry handler, etc. |
| `MeshMessageProcessorImpl` | `manager/MeshMessageProcessorImpl.kt` | Message persistence, notification dispatch, contact-aware filtering |
| `TelemetryPacketHandlerImpl` | `manager/TelemetryPacketHandlerImpl.kt` | Device/environment/power metric updates on nodes |
| `AdminPacketHandlerImpl` | `manager/AdminPacketHandlerImpl.kt` | Admin response processing, config/module updates |
| `NodeManagerImpl` | `manager/NodeManagerImpl.kt` | In-memory node cache + Room sync, identity updates, position tracking |
| `SessionManagerImpl` | `manager/SessionManagerImpl.kt` | Per-node remote-admin session tracking with TTL and passkey rotation |
| `CommandSenderImpl` | `manager/CommandSenderImpl.kt` | Constructs and sends admin/data packets to the radio |
| `MeshRouterImpl` | `manager/MeshRouterImpl.kt` | Service action routing (send message, request position, traceroute, etc.) |
| `MeshConfigHandlerImpl` | `manager/MeshConfigHandlerImpl.kt` | Config/module response processing and flow updates |
| `MeshConfigFlowManagerImpl` | `manager/MeshConfigFlowManagerImpl.kt` | Coroutine-based config request/response correlation |
| `MqttManagerImpl` | `manager/MqttManagerImpl.kt` | MQTT connection lifecycle management |
| `XModemManagerImpl` | `manager/XModemManagerImpl.kt` | XModem file transfer protocol for firmware updates |
| `HistoryManagerImpl` | `manager/HistoryManagerImpl.kt` | Sent-packet history for retry and deduplication |
| `MessageFilterImpl` | `manager/MessageFilterImpl.kt` | Message filtering (mute, ignore, contact-level rules) |
| `NodeRepositoryImpl` | `repository/NodeRepositoryImpl.kt` | Node CRUD, sort, filter, reactive flows |
| `PacketRepositoryImpl` | `repository/PacketRepositoryImpl.kt` | Message/packet CRUD, paging, unread counts |
| `RadioConfigRepositoryImpl` | `repository/RadioConfigRepositoryImpl.kt` | Radio config state flows (LoRa, device, display, etc.) |
| `FirmwareReleaseRepositoryImpl` | `repository/FirmwareReleaseRepositoryImpl.kt` | Firmware release data from JSON + local cache |
| `DeviceHardwareRepositoryImpl` | `repository/DeviceHardwareRepositoryImpl.kt` | Hardware catalog from JSON + local cache |
| `CoreDataModule` | `di/CoreDataModule.kt` | Koin module with component scan + explicit providers |
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST implement `MeshConnectionManager` interface with full connect/handshake/disconnect lifecycle.
- **FR-002**: System MUST process all `FromRadio` packet variants (MESH_PACKET, CONFIG_COMPLETE_ID, MY_INFO, NODE_INFO, METADATA, MQTTCLIENT_PROXY_MESSAGE, CLIENT_NOTIFICATION).
- **FR-003**: System MUST route decoded mesh packets by `PortNum` to the appropriate handler (text, telemetry, admin, traceroute, store-forward, neighbor-info, position).
- **FR-004**: System MUST persist messages as `Packet` entities in Room and emit notification events for new messages.
- **FR-005**: System MUST maintain an in-memory `Node` cache synchronized with Room, supporting reactive `Flow<List<Node>>` access.
- **FR-006**: System MUST track per-node remote-admin sessions with 300s TTL, passkey storage, and automatic expiry.
- **FR-007**: System MUST manage radio configuration state as `StateFlow` per config type (LoRa, device, display, network, Bluetooth, position, power, security).
- **FR-008**: System MUST support XModem file transfer for firmware updates via `XModemManager`.
- **FR-009**: System MUST provide `CommandSender` for constructing and sending admin/data packets to the radio.
- **FR-010**: System MUST filter messages based on contact settings (muted, ignored, message-filtering disabled).
- **FR-011**: System MUST manage MQTT connection lifecycle (connect, subscribe, publish, disconnect) aligned with radio config.
- **FR-012**: System MUST provide switching data sources between real and mock node-info read/write sources.
- **FR-013**: System MUST handle traceroute snapshot persistence and overlay construction.
- **FR-014**: System MUST maintain firmware release and device hardware catalogs with JSON + local DB caching.
### Non-Functional Requirements
- **NFR-001**: All implementations MUST reside in `commonMain` source set (Constitution §I).
- **NFR-002**: All coroutines MUST use named dispatchers from `CoroutineDispatchers` — no `Dispatchers.IO` directly (Constitution §VII).
- **NFR-003**: Error handling MUST use `safeCatching {}` where applicable (Constitution §VII).
- **NFR-004**: Session state MUST use `atomicfu` for thread-safe access to the `PersistentMap`.
- **NFR-005**: Database access via `withDb()` MUST tolerate connection-pool-closed races during DB switching.
- **NFR-006**: Node cache updates MUST be conflated to avoid overwhelming UI collectors during rapid mesh updates.
## Source-Set Impact
| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | 40 files (~10,500 LOC) | All manager and repository implementations |
| `commonTest` | 19 files (~1,700 LOC) | Unit tests for managers and repositories |
| `androidMain` | 0 files | No platform-specific code |
## Privacy Assessment
- [x] No PII, location data, or cryptographic keys logged — message content is never logged
- [x] Node identifiers are anonymized in log output via `anonymize()` utility
- [x] Proto submodule (`core/proto`) not modified (read-only upstream)
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: All 7 repository implementations pass their corresponding unit tests.
- **SC-002**: Connection lifecycle state machine correctly transitions through all states for BLE, TCP, and Serial transports.
- **SC-003**: Packet processing pipeline routes all known `PortNum` values to the correct handler.
- **SC-004**: Session manager correctly isolates per-node passkeys — concurrent sessions for 2+ nodes do not interfere.
- **SC-005**: `NodeManager.loadCachedNodeDB()` populates the in-memory cache from Room within 500ms for 100 nodes.
- **SC-006**: `MessageFilter` correctly suppresses notifications for muted/ignored contacts.
- **SC-007**: XModem transfer handles NAK retries and completes a simulated firmware file.
- **SC-008**: Config flow manager correlates request/response pairs within firmware's response timeout.
- **SC-009**: All 19 existing test files pass with `allTests` target.
- **SC-010**: `MqttManager` reconnects within 5s after network recovery.
## Assumptions
- All business logic resides in `commonMain` source set.
- Koin DI with `@ComponentScan` auto-discovers all `@Single`-annotated implementations.
- `core/repository` defines the interface contracts; this module provides the implementations.
- Room database access is via `DatabaseProvider.withDb()` — implementations never hold direct DAO references.
- The `Clock` dependency is injected for testability (no `System.currentTimeMillis()` calls).

View File

@@ -1,214 +0,0 @@
# Tasks: Core Data Layer
**Spec**: [spec.md](spec.md) | **Plan**: [plan.md](plan.md)
**Status**: Migrated — all existing tasks marked complete. Gap tasks marked incomplete.
**Task Prefix**: `DAT-T`
---
## Phase 1 — DI & Data Sources
### DAT-T001: Koin DI module setup [x]
- **File**: `core/data/src/commonMain/kotlin/org/meshtastic/core/data/di/CoreDataModule.kt`
- Created `CoreDataModule` with `@ComponentScan("org.meshtastic.core.data")`.
- Provides `MeshDataMapper` and `Clock.System` as explicit singletons.
- **Test**: Module loads without error in app startup.
### DAT-T002: Node info data source interfaces [x]
- **Files**: `datasource/NodeInfoReadDataSource.kt`, `NodeInfoWriteDataSource.kt`
- Defined read/write interfaces for node info access abstraction.
- **Test**: Interface contracts verified via implementations.
### DAT-T003: Switching data source implementations [x]
- **Files**: `datasource/SwitchingNodeInfoReadDataSource.kt`, `SwitchingNodeInfoWriteDataSource.kt`
- Delegates to real or mock data sources based on configuration.
- Enables mock mode for screenshots and UI testing.
- **Test**: Verified switching behavior in integration tests.
### DAT-T004: Firmware release JSON + local data sources [x]
- **Files**: `datasource/FirmwareReleaseJsonDataSource.kt`, `FirmwareReleaseLocalDataSource.kt`
- JSON parsing for GitHub release API responses.
- Room-backed local cache for offline access.
- **Test**: JSON parsing verified with sample payloads.
### DAT-T005: Device hardware JSON + local data sources [x]
- **Files**: `datasource/DeviceHardwareJsonDataSource.kt`, `DeviceHardwareLocalDataSource.kt`
- Hardware catalog parsing from JSON.
- Room-backed local cache.
- **Test**: JSON parsing verified with sample payloads.
### DAT-T006: Bootloader OTA quirks data source [x]
- **File**: `datasource/BootloaderOtaQuirksJsonDataSource.kt`
- Parses bootloader-specific OTA quirks for firmware update compatibility.
- **Test**: Verified with known quirk entries.
---
## Phase 2 — Repository Implementations
### DAT-T007: NodeRepositoryImpl [x]
- **File**: `repository/NodeRepositoryImpl.kt` (~291 LOC)
- Implements `NodeRepository` with reactive node list, sort, filter, identity updates.
- Uses `Lifecycle`-scoped coroutine sharing for node flows.
- **Test**: `CommonNodeRepositoryTest.kt` — covers node CRUD and flow emissions.
### DAT-T008: PacketRepositoryImpl [x]
- **File**: `repository/PacketRepositoryImpl.kt`
- Implements `PacketRepository` with paging support, unread counts, contact-aware queries.
- **Test**: `CommonPacketRepositoryTest.kt` — covers message persistence and queries.
### DAT-T009: RadioConfigRepositoryImpl [x]
- **File**: `repository/RadioConfigRepositoryImpl.kt`
- Manages `StateFlow` per config type (LoRa, device, display, network, etc.).
- **Test**: Verified via `MeshConfigHandlerImplTest.kt` integration.
### DAT-T010: FirmwareReleaseRepositoryImpl [x]
- **File**: `repository/FirmwareReleaseRepositoryImpl.kt`
- Fetches from remote, caches locally, exposes reactive flow.
- **Test**: Verified via integration with firmware update feature.
### DAT-T011: QuickChatActionRepositoryImpl [x]
- **File**: `repository/QuickChatActionRepositoryImpl.kt`
- CRUD for quick chat shortcuts with Room persistence.
- **Test**: Verified via messaging feature integration.
### DAT-T012: MeshLogRepositoryImpl [x]
- **File**: `repository/MeshLogRepositoryImpl.kt`
- Debug mesh log persistence with paging auto-eviction.
- **Test**: `CommonMeshLogRepositoryTest.kt`.
### DAT-T013: TracerouteSnapshotRepositoryImpl [x]
- **File**: `repository/TracerouteSnapshotRepositoryImpl.kt`
- Traceroute result persistence with position snapshots.
- **Test**: Verified via node detail metrics feature.
### DAT-T014: DeviceHardwareRepositoryImpl [x]
- **File**: `repository/DeviceHardwareRepositoryImpl.kt`
- Hardware catalog with JSON + Room caching.
- **Test**: Verified via device connections feature.
---
## Phase 3 — Manager Implementations: Connection & Packet Pipeline
### DAT-T015: MeshConnectionManagerImpl [x]
- **File**: `manager/MeshConnectionManagerImpl.kt` (~436 LOC)
- Full connection lifecycle: handshake, config exchange, state transitions.
- Coordinates with radio interface, node manager, notifications, analytics.
- **Test**: `MeshConnectionManagerImplTest.kt`.
### DAT-T016: FromRadioPacketHandlerImpl [x]
- **File**: `manager/FromRadioPacketHandlerImpl.kt`
- Top-level `FromRadio` proto dispatcher. Routes by `payloadVariant`.
- **Test**: `FromRadioPacketHandlerImplTest.kt`.
### DAT-T017: PacketHandlerImpl [x]
- **File**: `manager/PacketHandlerImpl.kt`
- Routes decoded `MeshPacket` by `PortNum` to specialized handlers.
- **Test**: `PacketHandlerImplTest.kt`.
### DAT-T018: MeshMessageProcessorImpl [x]
- **File**: `manager/MeshMessageProcessorImpl.kt`
- Persists text messages, triggers notifications, handles contact settings.
- **Test**: `MeshMessageProcessorImplTest.kt`.
### DAT-T019: TelemetryPacketHandlerImpl [x]
- **File**: `manager/TelemetryPacketHandlerImpl.kt`
- Updates device, environment, and power metrics on node entries.
- **Test**: `TelemetryPacketHandlerImplTest.kt`.
### DAT-T020: AdminPacketHandlerImpl [x]
- **File**: `manager/AdminPacketHandlerImpl.kt`
- Processes admin responses: config, module, channel, metadata.
- **Test**: `AdminPacketHandlerImplTest.kt`.
### DAT-T021: NodeManagerImpl [x]
- **File**: `manager/NodeManagerImpl.kt`
- In-memory node cache with Room synchronization and identity updates.
- **Test**: `NodeManagerImplTest.kt`.
### DAT-T022: CommandSenderImpl [x]
- **File**: `manager/CommandSenderImpl.kt`
- Constructs and sends admin/data packets to the radio.
- **Test**: Verified via `MeshActionHandlerImplTest.kt` integration.
---
## Phase 4 — Manager Implementations: Support Services
### DAT-T023: SessionManagerImpl [x]
- **File**: `manager/SessionManagerImpl.kt` (~118 LOC)
- Per-node remote-admin session tracking with `atomicfu` + `PersistentMap`.
- 300s TTL aligned with firmware, 240s active threshold.
- **Test**: `SessionManagerImplTest.kt`.
### DAT-T024: MeshConfigFlowManagerImpl [x]
- **File**: `manager/MeshConfigFlowManagerImpl.kt`
- Coroutine-based config request/response correlation with timeout.
- **Test**: `MeshConfigFlowManagerImplTest.kt`.
### DAT-T025: MessageFilterImpl + HistoryManagerImpl [x]
- **Files**: `manager/MessageFilterImpl.kt`, `manager/HistoryManagerImpl.kt`
- Mute/ignore/filter logic and sent-packet history for deduplication.
- **Test**: `MessageFilterImplTest.kt`, `HistoryManagerImplTest.kt`.
### DAT-T026: StoreForwardPacketHandlerImpl + NeighborInfoHandlerImpl [x]
- **Files**: `manager/StoreForwardPacketHandlerImpl.kt`, `manager/NeighborInfoHandlerImpl.kt`
- Store-and-forward history persistence, neighbor info upserts.
- **Test**: `StoreForwardPacketHandlerImplTest.kt`.
---
## Gap Tasks (Incomplete)
### DAT-T027: Add MqttManagerImpl unit tests [ ]
- **File to create**: `commonTest/.../manager/MqttManagerImplTest.kt`
- Cover connect/subscribe/publish/disconnect lifecycle.
- Verify reconnect on network recovery.
- **Priority**: Medium
### DAT-T028: Add MeshRouterImpl unit tests [x]
- **File to create**: `commonTest/.../manager/MeshRouterImplTest.kt`
- Cover service action routing: send message, request position, traceroute, admin commands.
- **Priority**: Medium
### DAT-T029: Add full pipeline integration test [ ]
- **File to create**: `commonTest/.../integration/PacketPipelineIntegrationTest.kt`
- End-to-end: raw `FromRadio` bytes → handler chain → persisted entity → notification.
- **Priority**: Medium
### DAT-T030: Add DataLayerHeartbeatSender + NeighborInfoHandler tests [ ]
- **Files to create**: `commonTest/.../manager/DataLayerHeartbeatSenderTest.kt`, `NeighborInfoHandlerImplTest.kt`
- Cover heartbeat interval, cancellation, neighbor info upsert.
- **Priority**: Low

View File

@@ -1,112 +0,0 @@
# Implementation Plan: Core BLE Abstraction
**Branch**: `013-core-ble` | **Date**: 2026-07-27 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/013-core-ble/spec.md`
**Status**: Migrated — all implementation complete, plan reverse-engineered from existing code.
## Summary
Core BLE wraps the Kable BLE library behind clean `commonMain` interfaces, providing device scanning, GATT connection management, characteristic I/O, exception classification, retry logic, and Meshtastic-specific BLE constants. Platform-specific code is minimal: Android (3 files), JVM (2 files), iOS (1 noop file).
## Technical Context
**Language/Version**: Kotlin 2.3+ targeting JDK 21
**Primary Dependencies**: Kable (BLE), Koin 4.2+, kotlinx.coroutines, Kermit logging
**Testing**: KMP `allTests` — 5 test files, ~300 LOC
**Target Platform**: Android, Desktop (JVM), iOS
**Constraints**: Kable types must not leak into public interfaces; all shared code in `commonMain`
**Scale/Scope**: 22 commonMain files (~1,800 LOC), 6 platform files (~300 LOC), 5 test files (~300 LOC)
## Constitution Check
*GATE: All checks pass — existing production code reviewed against Constitution v1.2.2.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Kotlin Multiplatform Core | ✅ PASS | All interfaces in `commonMain`. Platform code minimal and correctly scoped. |
| II. Zero Lint Tolerance | ✅ PASS | `detekt-baseline.xml` present. `@Suppress` used sparingly. |
| III. Compose Multiplatform UI | N/A | No UI code. |
| IV. Privacy First | ✅ PASS | No PII logged. MAC addresses suppressed in release. |
| V. Design Standards Compliance | N/A | No UI code. |
| VI. Verify Before Push | ⚠️ PARTIAL | 5 test files cover utilities, but no integration test for `KableBleConnection`. |
| VII. Coroutine Safety | ✅ PASS | `retryBleOperation` re-throws `CancellationException`. Named dispatchers used. |
| VIII. Resource Discipline | N/A | No resources. |
| IX. Branch & Scope Hygiene | ✅ PASS | Module cleanly scoped to `org.meshtastic.core.ble`. |
**Gate Result**: ✅ All applicable principles satisfied (1 test coverage gap noted).
## Project Structure
```
core/ble/src/
├── commonMain/kotlin/org/meshtastic/core/ble/
│ ├── di/CoreBleModule.kt
│ ├── BleScanner.kt # Interface
│ ├── KableBleScanner.kt # Kable implementation
│ ├── BleConnection.kt # Interface + BleService interface
│ ├── KableBleConnection.kt # Kable implementation (276 LOC)
│ ├── ActiveBleConnection.kt # Active-state tracking wrapper
│ ├── BleConnectionFactory.kt # Factory interface
│ ├── KableBleConnectionFactory.kt # Kable factory
│ ├── BleConnectionState.kt # Sealed class
│ ├── KableStateMapping.kt # Kable → BleConnectionState
│ ├── BleExceptionClassifier.kt # Exception → BleExceptionInfo
│ ├── BleRetry.kt # Exponential backoff retry
│ ├── BleDevice.kt # Device representation
│ ├── MeshtasticBleDevice.kt # Meshtastic-specific device
│ ├── MeshtasticRadioProfile.kt # Profile interface
│ ├── KableMeshtasticRadioProfile.kt # Kable profile implementation
│ ├── MeshtasticBleConstants.kt # UUIDs and constants
│ ├── BluetoothRepository.kt # BT adapter state interface
│ ├── BleLoggingConfig.kt # Debug/Release logging
│ ├── KermitLogEngine.kt # Kable → Kermit bridge
│ ├── BleServiceExtensions.kt # Utility extensions
│ └── KablePlatformSetup.kt # expect declaration
├── commonTest/kotlin/org/meshtastic/core/ble/
│ ├── BleExceptionClassifierTest.kt
│ ├── KableMeshtasticRadioProfileTest.kt
│ ├── KableStateMappingTest.kt
│ ├── DisconnectReasonTest.kt
│ └── BleRetryTest.kt
├── androidMain/ (3 files — BluetoothRepository, PlatformSetup, DI)
├── jvmMain/ (2 files — BluetoothRepository, PlatformSetup)
└── iosMain/ (1 file — NoopStubs)
```
## Implementation Phases
### Phase 1 — Interfaces & Constants (Complete)
Defined all `commonMain` interfaces (`BleScanner`, `BleConnection`, `BleService`, `BleConnectionFactory`, `BluetoothRepository`) and Meshtastic BLE constants.
### Phase 2 — Kable Implementations (Complete)
Built `KableBleScanner`, `KableBleConnection` (276 LOC), `KableBleConnectionFactory`, `KableBleService`, `KableStateMapping`. Implemented `BleExceptionClassifier` and `BleRetry`.
### Phase 3 — Platform Integration (Complete)
Platform-specific `BluetoothRepository` implementations (Android wraps `BluetoothAdapter`, JVM/iOS provide stubs). `KablePlatformSetup` expect/actual for platform scanner/peripheral configuration.
## Technical Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| BLE library | Kable | Best KMP BLE library with coroutine-first API |
| Abstraction strategy | Interface wrapping | Prevents Kable type leakage; enables fake injection |
| Retry strategy | Exponential backoff + jitter | Prevents retry storms from synchronized BLE failures |
| Backoff cap | 2 seconds | Balances retry speed with BLE stack recovery time |
| Jitter range | ±25% | Decorrelates concurrent retries without excessive randomness |
| State mapping | Extension function on Kable `State` | Clean, testable, single point of conversion |
| Logging bridge | `KermitLogEngine` | Unifies Kable and app logging under Kermit |
| Debug logging | Verbose Kable Events in debug only | Prevents log spam in release; enables deep debugging |
## Gaps Identified
| Gap | Severity | Recommendation |
|-----|----------|----------------|
| No integration test for `KableBleConnection` | ⚠️ Medium | Add connected lifecycle test with mock Kable `Peripheral` |
| `KableBleScanner` has no unit test | ⚠️ Medium | Add test for scan flow emissions and timeout |
| `ActiveBleConnection` has no unit test | ⚠️ Low | Add test for active-state tracking behavior |
| No test for `KableBleConnectionFactory.create()` | ⚠️ Low | Verify factory produces correctly-scoped connections |
| iOS implementation is noop stubs | Info | Will need real implementation when iOS BLE stabilizes |

View File

@@ -1,195 +0,0 @@
# Feature Specification: Core BLE Abstraction
**Feature Branch**: `013-core-ble`
**Created**: 2026-07-27
**Status**: Migrated
**Input**: Brownfield migration — reverse-engineered from existing `core/ble` module
## Summary
Core BLE provides a platform-agnostic Bluetooth Low Energy abstraction layer for Meshtastic-Android. It wraps the Kable library behind clean `commonMain` interfaces (`BleScanner`, `BleConnection`, `BleConnectionFactory`, `BleService`, `BluetoothRepository`) so that consumers (`core/network`, `feature/wifi-provision`, `feature/device-connections`) never depend on Kable directly. The module handles device scanning, GATT connection lifecycle, characteristic read/write/observe, connection state mapping, exception classification, retry with exponential backoff, and Meshtastic-specific BLE constants (service UUID, characteristic UUIDs). Platform-specific implementations exist in `androidMain` (Android `BluetoothAdapter` integration), `jvmMain` (desktop stubs), and `iosMain` (noop stubs).
## Goals
1. **Platform abstraction** — isolate Kable (and platform BLE APIs) behind `commonMain` interfaces so transport consumers don't import Kable types.
2. **Reliable connections** — provide exponential-backoff retry, structured exception classification, and connection state mapping for robust BLE operations.
3. **Meshtastic radio profile** — encapsulate Meshtastic-specific GATT service/characteristic UUIDs and MTU constants.
4. **Reusable scanning** — provide a generic `BleScanner` that supports service UUID filtering, address-based targeting, and timeout-based scan windows.
5. **Testable** — enable consumers to inject `BleConnectionFactory` and `BleScanner` fakes for unit testing without real hardware.
## Non-Goals
- Transport-level framing or protobuf encoding — handled by `core/network`.
- WiFi provisioning protocol (nymea) — handled by `feature/wifi-provision` (uses `BleConnection` + `BleService`).
- MQTT or TCP connectivity — this module is BLE-only.
- Android runtime permission management — handled by the app module.
## User Scenarios & Testing *(mandatory)*
### User Story 1 — Scan for Meshtastic BLE Devices (Priority: P1)
The BLE scanner discovers nearby Meshtastic devices advertising the Meshtastic service UUID. Results are emitted as a cold `Flow<BleDevice>` that terminates after the scan timeout.
**Why this priority**: Device discovery is the prerequisite for all BLE connectivity.
**Independent Test**: Can be unit-tested with Kable scanner mocks.
**Acceptance Scenarios**:
1. **Given** Bluetooth is enabled, **When** `scan(timeout, serviceUuid)` is called, **Then** a flow of `BleDevice` objects is emitted for each matching advertisement.
2. **Given** a specific MAC address is provided, **When** `scan(timeout, address = "AA:BB:CC")` is called, **Then** only the matching device is emitted.
3. **Given** the scan timeout elapses, **When** no more devices are found, **Then** the flow completes normally.
4. **Given** Bluetooth is disabled, **When** scan is attempted, **Then** the flow terminates with an appropriate exception.
---
### User Story 2 — Connect to a BLE Device (Priority: P1)
`BleConnection` manages the GATT connection lifecycle. Consumers call `connect(device)` or `connectAndAwait(device, timeout)` and observe `connectionState: StateFlow<BleConnectionState>` for state transitions.
**Why this priority**: Active BLE connection is required for all radio communication.
**Independent Test**: Connection state transitions can be tested by observing the `connectionState` flow.
**Acceptance Scenarios**:
1. **Given** a valid `BleDevice`, **When** `connect(device)` is called, **Then** `connectionState` transitions from `Disconnected``Connecting``Connected`.
2. **Given** `connectAndAwait(device, 30s)` is called, **When** connection succeeds within timeout, **Then** it returns `BleConnectionState.Connected`.
3. **Given** the timeout elapses before connection, **When** `connectAndAwait` returns, **Then** it returns the current disconnected state.
4. **Given** the remote device disconnects, **When** the GATT disconnection event occurs, **Then** `connectionState` transitions to `Disconnected` with a `DisconnectReason`.
---
### User Story 3 — Read/Write/Observe GATT Characteristics (Priority: P1)
Within a connected `BleService` profile scope, consumers can observe notifications, read values, and write data to characteristics using the `BleService` interface.
**Why this priority**: All mesh data exchange happens through characteristic read/write/observe.
**Independent Test**: Can be validated by writing to a characteristic and observing the echo.
**Acceptance Scenarios**:
1. **Given** an active `BleService` for the Meshtastic service UUID, **When** `observe(fromRadio)` is called, **Then** a flow of `ByteArray` notifications is emitted.
2. **Given** `observe(characteristic, onSubscription)` is used, **When** CCCD write completes, **Then** `onSubscription` is invoked before the first notification.
3. **Given** a payload to send, **When** `write(toRadio, data, WITH_RESPONSE)` is called, **Then** the data is written with write-with-response semantics.
4. **Given** the connection drops during a write, **When** the exception is caught, **Then** `classifyBleException()` returns a `BleExceptionInfo` with a meaningful message.
---
### User Story 4 — BLE Operation Retry with Backoff (Priority: P2)
The `retryBleOperation` utility retries transient BLE failures with bounded exponential backoff and jitter to avoid retry storms.
**Why this priority**: BLE operations are inherently unreliable. Retry logic is essential for production stability.
**Independent Test**: Fully testable in isolation with simulated failures.
**Acceptance Scenarios**:
1. **Given** a BLE operation fails on the first attempt, **When** retry is invoked with `count=3`, **Then** it retries up to 2 more times with increasing delay.
2. **Given** all 3 attempts fail, **When** the last attempt throws, **Then** the exception is propagated to the caller.
3. **Given** a `CancellationException` is thrown, **When** caught by retry, **Then** it is immediately re-thrown (structured concurrency preserved).
4. **Given** backoff delay exceeds `MAX_RETRY_DELAY_MS` (2s), **When** calculated, **Then** the delay is capped at 2s with ±25% jitter.
---
### Edge Cases
- What happens when `maximumWriteValueLength()` returns null? The caller falls back to `DEFAULT_BLE_WRITE_VALUE_LENGTH` (20 bytes).
- What happens when `requestHighConnectionPriority()` is called on a non-Android platform? It returns `false` (default implementation).
- What happens when a GATT status error with an unknown code is classified? `BleExceptionInfo` is returned with the raw status code.
- What happens when multiple `observe()` collectors exist on the same characteristic? Each gets an independent flow backed by the same Kable observation.
## Architecture
### Key Components
| Component | File | Purpose |
|-----------|------|---------|
| `BleScanner` | `BleScanner.kt` | Interface: scans for BLE devices with timeout & filtering |
| `KableBleScanner` | `KableBleScanner.kt` | Kable-backed scanner implementation |
| `BleConnection` | `BleConnection.kt` | Interface: GATT connection lifecycle, state, characteristic access |
| `KableBleConnection` | `KableBleConnection.kt` | Kable `Peripheral`-backed connection (276 LOC) |
| `ActiveBleConnection` | `ActiveBleConnection.kt` | Connection wrapper with active-state tracking |
| `BleConnectionFactory` | `BleConnectionFactory.kt` | Factory interface for creating `BleConnection` instances |
| `KableBleConnectionFactory` | `KableBleConnectionFactory.kt` | Kable-backed factory |
| `BleService` | `BleConnection.kt` | Interface: characteristic observe/read/write within a GATT profile |
| `KableBleService` | `KableBleConnection.kt` | Kable `Peripheral`-backed service implementation |
| `BleConnectionState` | `BleConnectionState.kt` | Sealed class: Connected / Disconnected(reason) / Connecting |
| `KableStateMapping` | `KableStateMapping.kt` | Maps Kable `State``BleConnectionState` |
| `BleExceptionClassifier` | `BleExceptionClassifier.kt` | Classifies Kable exceptions into `BleExceptionInfo` |
| `BleRetry` | `BleRetry.kt` | Exponential backoff retry with jitter |
| `MeshtasticRadioProfile` | `MeshtasticRadioProfile.kt` | Meshtastic service/characteristic UUID profile |
| `KableMeshtasticRadioProfile` | `KableMeshtasticRadioProfile.kt` | Kable-specific profile implementation |
| `MeshtasticBleConstants` | `MeshtasticBleConstants.kt` | Service UUID, characteristic UUIDs, MTU constants |
| `MeshtasticBleDevice` | `MeshtasticBleDevice.kt` | Meshtastic-specific BLE device wrapper |
| `BleDevice` | `BleDevice.kt` | Platform-agnostic BLE device representation |
| `BluetoothRepository` | `BluetoothRepository.kt` | Bluetooth adapter state (enabled/disabled) |
| `BleLoggingConfig` | `BleLoggingConfig.kt` | Debug vs release logging configuration |
| `KermitLogEngine` | `KermitLogEngine.kt` | Bridges Kable logging to Kermit |
| `BleServiceExtensions` | `BleServiceExtensions.kt` | Extension functions for `BleService` |
| `CoreBleModule` | `di/CoreBleModule.kt` | Koin DI module |
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST provide a `BleScanner` interface that emits `Flow<BleDevice>` from a time-bounded scan.
- **FR-002**: System MUST support scan filtering by service UUID and/or MAC address.
- **FR-003**: System MUST provide a `BleConnection` interface with `connect`, `connectAndAwait`, `disconnect`, and `connectionState` flow.
- **FR-004**: System MUST provide a `BleService` interface with `observe`, `read`, `write`, `hasCharacteristic`, and `preferredWriteType`.
- **FR-005**: System MUST support `observe(characteristic, onSubscription)` to execute a callback after CCCD write completes.
- **FR-006**: System MUST provide a `BleConnectionFactory` for creating `BleConnection` instances scoped to a `CoroutineScope`.
- **FR-007**: System MUST classify Kable exceptions (`GattStatusException`, `NotConnectedException`, `GattRequestRejectedException`, `UnmetRequirementException`) into `BleExceptionInfo`.
- **FR-008**: System MUST provide `retryBleOperation` with configurable count, initial delay, exponential backoff (factor 2), 2s cap, and ±25% jitter.
- **FR-009**: System MUST map Kable `State` values to `BleConnectionState` (Connected, Connecting, Disconnected with reason).
- **FR-010**: System MUST define Meshtastic GATT constants: service UUID (`0xfeb8`), `FromRadio`, `ToRadio`, `FromNum`, `LogRadio` characteristic UUIDs.
- **FR-011**: System MUST provide `requestHighConnectionPriority()` with platform-specific implementation on Android (default `false` on other platforms).
- **FR-012**: System MUST bridge Kable logging to Kermit via `KermitLogEngine`, with verbose logging in debug builds only.
### Non-Functional Requirements
- **NFR-001**: All interfaces and shared logic MUST reside in `commonMain` (Constitution §I).
- **NFR-002**: Kable types (`Peripheral`, `Scanner`, `State`) MUST NOT leak into public API surfaces.
- **NFR-003**: `retryBleOperation` MUST re-throw `CancellationException` immediately (Constitution §VII).
- **NFR-004**: BLE logging MUST be single-line format for logcat/grep friendliness.
- **NFR-005**: Default write value length MUST be 20 bytes (23-byte ATT MTU minus 3-byte header).
## Source-Set Impact
| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | 22 files (~1,800 LOC) | All interfaces, Kable implementations, constants, retry logic |
| `commonTest` | 5 files (~300 LOC) | Tests for exception classifier, state mapping, radio profile, retry, disconnect reason |
| `androidMain` | 3 files (~200 LOC) | `AndroidBluetoothRepository`, platform-specific `KablePlatformSetup`, Android DI module |
| `jvmMain` | 2 files (~80 LOC) | Desktop `KableBluetoothRepository`, `KablePlatformSetup` |
| `iosMain` | 1 file (~20 LOC) | Noop stubs |
## Privacy Assessment
- [x] No PII logged — BLE device addresses are not logged in release builds
- [x] No user data transmitted — BLE module handles raw byte transport only
- [x] Proto submodule not modified
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: `BleExceptionClassifier` correctly classifies all 4 Kable exception types into `BleExceptionInfo`.
- **SC-002**: `KableStateMapping` maps all Kable `State` values to the correct `BleConnectionState`.
- **SC-003**: `retryBleOperation` retries exactly `count-1` times on transient failures, with delays capped at 2s.
- **SC-004**: `KableMeshtasticRadioProfile` correctly resolves all 4 Meshtastic characteristic UUIDs.
- **SC-005**: Disconnect reason mapping produces meaningful human-readable messages for all known Kable disconnect states.
- **SC-006**: All 5 existing test files pass with `allTests` target.
- **SC-007**: BLE module compiles for all 3 targets (Android, JVM, iOS) with no platform leaks.
- **SC-008**: Debug builds produce verbose Kable logs; release builds produce quiet logs.
## Assumptions
- Kable library is the sole BLE implementation backing — no fallback to raw platform APIs.
- Consumers inject `BleConnectionFactory`/`BleScanner` via Koin; fakes are available in `core/testing`.
- MTU negotiation is not performed — the module assumes minimum 20-byte write value length.
- iOS implementation is currently noop stubs (iOS BLE support is pending full Kable iOS stabilization).
- `BleLoggingConfig` is provided via Koin based on `BuildConfigProvider.isDebug`.

View File

@@ -1,163 +0,0 @@
# Tasks: Core BLE Abstraction
**Spec**: [spec.md](spec.md) | **Plan**: [plan.md](plan.md)
**Status**: Migrated — all existing tasks marked complete. Gap tasks marked incomplete.
**Task Prefix**: `BLE-T`
---
## Phase 1 — Interfaces & Constants
### BLE-T001: BLE device and connection state types [x]
- **Files**: `BleDevice.kt`, `MeshtasticBleDevice.kt`, `BleConnectionState.kt`
- Defined `BleDevice` (address, name, rssi), `MeshtasticBleDevice` (Meshtastic-specific wrapper), `BleConnectionState` sealed class (Connected, Connecting, Disconnected with reason).
- **Test**: `DisconnectReasonTest.kt` — covers disconnect reason mapping.
### BLE-T002: BleScanner interface [x]
- **File**: `BleScanner.kt`
- Defined `scan(timeout, serviceUuid?, address?)` returning `Flow<BleDevice>`.
- **Test**: Interface contract verified via consumers.
### BLE-T003: BleConnection + BleService interfaces [x]
- **File**: `BleConnection.kt` (~114 LOC)
- `BleConnection`: connect, connectAndAwait, disconnect, profile, connectionState, deviceFlow.
- `BleService`: observe, read, write, hasCharacteristic, preferredWriteType.
- `BleWriteType` enum: WITH_RESPONSE, WITHOUT_RESPONSE.
- `BleCharacteristic` data class.
- **Test**: Interface contracts verified via implementations.
### BLE-T004: BleConnectionFactory interface [x]
- **File**: `BleConnectionFactory.kt`
- Factory: `create(scope, tag)``BleConnection`.
- **Test**: Verified via `core/network` BLE transport.
### BLE-T005: BluetoothRepository interface [x]
- **File**: `BluetoothRepository.kt`
- Bluetooth adapter state (enabled/disabled) as reactive flow.
- **Test**: Verified via consumers.
### BLE-T006: Meshtastic BLE constants [x]
- **File**: `MeshtasticBleConstants.kt`
- Service UUID (`0xfeb8`), `FromRadio`, `ToRadio`, `FromNum`, `LogRadio` characteristic UUIDs.
- **Test**: `KableMeshtasticRadioProfileTest.kt` validates UUID resolution.
### BLE-T007: MeshtasticRadioProfile interface + Kable implementation [x]
- **Files**: `MeshtasticRadioProfile.kt`, `KableMeshtasticRadioProfile.kt`
- Maps Meshtastic characteristic names to `BleCharacteristic` instances.
- **Test**: `KableMeshtasticRadioProfileTest.kt`.
---
## Phase 2 — Kable Implementations
### BLE-T008: KableBleScanner [x]
- **File**: `KableBleScanner.kt`
- Wraps Kable `Scanner` with service UUID filtering and timeout.
- Emits `BleDevice` for each discovered advertisement.
- **Test**: Verified via integration with device connections feature.
### BLE-T009: KableBleConnection + KableBleService [x]
- **File**: `KableBleConnection.kt` (~276 LOC)
- `KableBleConnection`: manages Kable `Peripheral` lifecycle, state observation, disconnect handling.
- `KableBleService`: wraps `Peripheral` for per-service characteristic I/O.
- Connection state observation via `Peripheral.state``BleConnectionState` mapping.
- **Test**: State mapping verified via `KableStateMappingTest.kt`.
### BLE-T010: ActiveBleConnection [x]
- **File**: `ActiveBleConnection.kt`
- Thin wrapper tracking whether the connection is actively being used.
- **Test**: Verified via integration.
### BLE-T011: KableBleConnectionFactory [x]
- **File**: `KableBleConnectionFactory.kt`
- Creates `KableBleConnection` instances scoped to a `CoroutineScope`.
- **Test**: Verified via `BleRadioTransport` in `core/network`.
### BLE-T012: KableStateMapping [x]
- **File**: `KableStateMapping.kt`
- Maps Kable `State.Connected``BleConnectionState.Connected`, etc.
- Clean extension function approach.
- **Test**: `KableStateMappingTest.kt` — covers all state transitions.
### BLE-T013: BleExceptionClassifier [x]
- **File**: `BleExceptionClassifier.kt` (~65 LOC)
- `Throwable.classifyBleException()``BleExceptionInfo?`
- Classifies: `GattStatusException`, `NotConnectedException`, `GattRequestRejectedException`, `UnmetRequirementException`.
- All currently classified as transient (`isPermanent = false`).
- **Test**: `BleExceptionClassifierTest.kt` — covers all 4 exception types + unknown.
### BLE-T014: BleRetry with exponential backoff [x]
- **File**: `BleRetry.kt` (~73 LOC)
- `retryBleOperation(count, delayMs, tag, block)`
- Backoff factor 2, cap at 2s, ±25% jitter.
- Re-throws `CancellationException` immediately.
- **Test**: `BleRetryTest.kt` — covers success, retry, exhaustion, cancellation.
### BLE-T015: BLE logging infrastructure [x]
- **Files**: `BleLoggingConfig.kt`, `KermitLogEngine.kt`
- `BleLoggingConfig.Debug` (verbose Kable Events) vs `BleLoggingConfig.Release` (quiet).
- `KermitLogEngine` bridges Kable logging to Kermit.
- **Test**: Configuration verified via `CoreBleModule` provider.
### BLE-T016: BleServiceExtensions [x]
- **File**: `BleServiceExtensions.kt`
- Utility extension functions for common `BleService` operations.
- **Test**: Verified via consumers.
---
## Phase 3 — Platform Integration
### BLE-T017: Android BluetoothRepository + DI [x]
- **Files**: `androidMain/.../AndroidBluetoothRepository.kt`, `di/CoreBleAndroidModule.kt`, `KablePlatformSetup.kt`
- Wraps `BluetoothAdapter` for adapter state observation.
- Android-specific Kable scanner/peripheral configuration.
- **Test**: Verified via Android app integration.
### BLE-T018: JVM + iOS platform stubs [x]
- **Files**: `jvmMain/.../KableBluetoothRepository.kt`, `KablePlatformSetup.kt`, `iosMain/.../NoopStubs.kt`
- JVM: Desktop Bluetooth repository with Kable desktop scanner.
- iOS: Noop stubs (pending full Kable iOS support).
- **Test**: Compilation verified on all targets.
---
## Gap Tasks (Incomplete)
### BLE-T019: Add KableBleConnection integration tests [ ]
- **File to create**: `commonTest/.../KableBleConnectionTest.kt`
- Test connected lifecycle with mock Kable `Peripheral`.
- Verify state transitions, profile access, disconnect handling.
- **Priority**: Medium
### BLE-T020: Add KableBleScanner unit tests [x]
- **File to create**: `commonTest/.../KableBleConnectionTest.kt`
- Test scan flow emissions, timeout behavior, service UUID filtering.
- **Priority**: Medium
### BLE-T021: Add ActiveBleConnection tests [ ]
- **File to create**: `commonTest/.../ActiveBleConnectionTest.kt`
- Verify active-state tracking and delegation behavior.
- **Priority**: Low

View File

@@ -1,116 +0,0 @@
# Implementation Plan: Core Network & Radio Transport
**Branch**: `014-core-network` | **Date**: 2026-07-27 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/014-core-network/spec.md`
**Status**: Migrated — all implementation complete, plan reverse-engineered from existing code.
## Summary
Core Network implements the multi-transport radio architecture (BLE, TCP, Serial, Mock), Meshtastic stream framing, MQTT mesh bridging, network monitoring, mDNS service discovery, and HTTP client infrastructure. The BLE transport (506 LOC) is the most complex component, with automatic reconnection and firmware handshake awareness.
## Technical Context
**Language/Version**: Kotlin 2.3+ targeting JDK 21
**Primary Dependencies**: Kable (via `core/ble`), Ktor (HTTP), kotlinx.serialization, kotlinx.coroutines, Kermit, MQTT client library
**Testing**: KMP `allTests` — 7 commonTest + 1 jvmTest files, ~700 LOC; Turbine, Mokkery
**Target Platform**: Android, Desktop (JVM), iOS (partial)
**Constraints**: BLE/MQTT/stream logic in `commonMain`; TCP in `jvmAndroidMain`; Serial in `androidMain`
**Scale/Scope**: 23 commonMain files (~3,200 LOC), 16 platform files (~1,650 LOC), 8 test files (~700 LOC)
## Constitution Check
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Kotlin Multiplatform Core | ✅ PASS | BLE transport, stream codec, MQTT in `commonMain`. Serial/USB correctly in `androidMain`. |
| II. Zero Lint Tolerance | ✅ PASS | `detekt-baseline.xml` present. Suppressions: `TooManyFunctions`, `TooGenericExceptionCaught`. |
| III. Compose Multiplatform UI | N/A | No UI code. |
| IV. Privacy First | ✅ PASS | Device addresses not logged; MQTT credentials suppressed. |
| VI. Verify Before Push | ⚠️ PARTIAL | Good coverage for BLE transport and stream codec; gaps in TCP, Serial, Mock. |
| VII. Coroutine Safety | ✅ PASS | `safeCatching` in MQTT; `NonCancellable` for disconnect cleanup; mutex for codec writes. |
| IX. Branch & Scope Hygiene | ✅ PASS | Module cleanly scoped. Transport factory pattern enables extension. |
**Gate Result**: ✅ All applicable principles satisfied.
## Project Structure
```
core/network/src/
├── commonMain/kotlin/org/meshtastic/core/network/
│ ├── di/CoreNetworkModule.kt
│ ├── radio/
│ │ ├── BleRadioTransport.kt # 506 LOC — BLE transport with reconnect
│ │ ├── BleReconnectPolicy.kt # Backoff configuration
│ │ ├── StreamTransport.kt # Base for framed transports
│ │ ├── MockRadioTransport.kt # Test/demo transport
│ │ ├── NopRadioTransport.kt # No-op transport
│ │ └── BaseRadioTransportFactory.kt # Factory for transport creation
│ ├── transport/
│ │ ├── StreamFrameCodec.kt # 154 LOC — START1/START2 framing
│ │ └── HeartbeatSender.kt # Keep-alive for stream transports
│ ├── repository/
│ │ ├── MQTTRepositoryImpl.kt # 312 LOC — MQTT lifecycle
│ │ ├── MQTTRepository.kt # Interface
│ │ ├── NetworkRepositoryImpl.kt # Network + discovery flows
│ │ ├── NetworkRepository.kt # Interface
│ │ ├── NetworkMonitor.kt # Connectivity interface
│ │ ├── ServiceDiscovery.kt # mDNS interface
│ │ ├── DiscoveredService.kt # Service discovery model
│ │ ├── NetworkConstants.kt # Network-related constants
│ │ └── KermitMqttLogger.kt # MQTT → Kermit logging bridge
│ ├── service/ApiService.kt # REST API abstraction
│ ├── HttpClientDefaults.kt # Ktor client configuration
│ ├── KermitHttpLogger.kt # Ktor → Kermit logging
│ ├── FirmwareReleaseRemoteDataSource.kt
│ └── DeviceHardwareRemoteDataSource.kt
├── commonTest/ (7 files — BLE transport, reconnect, stream codec, MQTT)
├── jvmAndroidMain/ (2 files — TCP transport + socket)
├── jvmMain/ (3 files — network monitor, service discovery, serial)
├── jvmTest/ (1 file — service discovery)
└── androidMain/ (14 files — USB/Serial, NSD, connectivity, DI)
```
## Implementation Phases
### Phase 1 — Transport Interfaces & Stream Codec (Complete)
Core abstractions: `RadioTransport` interface, `StreamFrameCodec` (START1/START2 protocol), `HeartbeatSender`, `NopRadioTransport`.
### Phase 2 — BLE Radio Transport (Complete)
The primary transport: `BleRadioTransport` (506 LOC) with scan → connect → profile discovery → observation pipeline. `BleReconnectPolicy` for automatic reconnection with configurable exponential backoff and rate limiting.
### Phase 3 — Secondary Transports (Complete)
TCP transport (`jvmAndroidMain`), Serial/USB transport (`androidMain`), Mock transport for testing. Transport factory for runtime transport selection.
### Phase 4 — MQTT & Network Infrastructure (Complete)
`MQTTRepositoryImpl` (312 LOC) with broker connection, topic management, protobuf/JSON decoding. Network monitoring, mDNS service discovery, and HTTP client for API access.
## Technical Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Transport interface | Unified `RadioTransport` | All transports share the same contract for data flow |
| BLE reconnect | Configurable `BleReconnectPolicy` | Allows tuning backoff for different use cases |
| Stream framing | Byte-at-a-time state machine | Handles partial reads and stream corruption recovery |
| Frame max size | 512 bytes (`MAX_TO_FROM_RADIO_SIZE`) | Matches firmware's maximum protobuf message size |
| TCP port | 4403 (hardcoded) | Standard Meshtastic TCP service port |
| MQTT library | Wrapped `MqttClient` from `:mqtt` module | Isolates MQTT library choice from the network layer |
| JSON config | Lenient + `ignoreUnknownKeys` | Tolerates server-side schema changes |
| Write thread safety | Mutex in `StreamFrameCodec` | Prevents interleaved frame corruption |
| Wake bytes | 4x START1 before TCP connect | Rouses sleeping Meshtastic devices |
| Reconnect rate limit | >5 attempts in 30s | Prevents aggressive retry loops that drain battery |
## Gaps Identified
| Gap | Severity | Recommendation |
|-----|----------|----------------|
| `TcpRadioTransport` has no unit test | ⚠️ Medium | Add test with loopback server |
| `SerialRadioTransport` has no unit test | ⚠️ Medium | Add Android instrumented test |
| `MockRadioTransport` has no unit test | ⚠️ Low | Trivial but documents the mock contract |
| MQTT test coverage is minimal (1 file) | ⚠️ Medium | Add tests for topic management, JSON/protobuf decode, reconnect |
| `HeartbeatSender` has no unit test | ⚠️ Low | Add test for interval and cancellation |
| No end-to-end transport integration test | ⚠️ Medium | Test: create transport → connect → send → receive → disconnect |
| `FirmwareReleaseRemoteDataSource` has no test | ⚠️ Low | Add test with Ktor mock engine |

View File

@@ -1,219 +0,0 @@
# Feature Specification: Core Network & Radio Transport
**Feature Branch**: `014-core-network`
**Created**: 2026-07-27
**Status**: Migrated
**Input**: Brownfield migration — reverse-engineered from existing `core/network` module
## Summary
Core Network provides the radio transport layer, MQTT connectivity, network monitoring, service discovery, and HTTP client infrastructure for Meshtastic-Android. The module implements a multi-transport architecture supporting BLE, TCP, Serial, and Mock radios through a unified `RadioTransport` interface. It includes the Meshtastic stream framing codec (START1/START2 protocol), BLE reconnect policy with exponential backoff, MQTT client integration for mesh-to-internet bridging, mDNS/NSD service discovery for local Meshtastic devices, network connectivity monitoring, and HTTP client configuration for firmware/hardware API access. Platform-specific code handles Android USB/Serial, mDNS, and network monitoring, while the transport core is in `commonMain`.
## Goals
1. **Multi-transport radio connectivity** — support BLE, TCP, Serial (Android), and Mock transports through a unified `RadioTransport` interface.
2. **Reliable BLE transport** — implement automatic reconnection with configurable backoff, rate limiting, and firmware handshake awareness.
3. **Stream framing** — encode/decode the Meshtastic START1/START2 + length-prefix protocol for serial and TCP byte streams.
4. **MQTT mesh bridging** — connect to MQTT brokers for mesh-to-internet packet relay with topic-based routing.
5. **Network awareness** — monitor connectivity state and discover local Meshtastic devices via mDNS/NSD.
6. **API client** — configure Ktor HTTP client for firmware release and device hardware catalog fetches.
## Non-Goals
- BLE scanning and GATT abstraction — handled by `core/ble`.
- Packet decoding or protobuf parsing — handled by `core/data`.
- Radio configuration management — handled by `core/data` repositories.
- UI for transport selection — handled by `feature/device-connections`.
## User Scenarios & Testing *(mandatory)*
### User Story 1 — BLE Radio Transport Connection (Priority: P1)
The BLE radio transport scans for a Meshtastic device, establishes a GATT connection, discovers the Meshtastic service profile, and sets up characteristic observation for incoming radio data. It handles automatic reconnection on disconnection with configurable backoff.
**Why this priority**: BLE is the primary transport for mobile users. ~90% of connections use BLE.
**Independent Test**: Can be validated with mock `BleConnection` and `BleScanner` fakes.
**Acceptance Scenarios**:
1. **Given** a Meshtastic device is advertising, **When** `BleRadioTransport.connect(address)` is called, **Then** the transport scans, connects, discovers the service profile, and begins observing `FromRadio`.
2. **Given** the BLE connection drops, **When** `BleReconnectPolicy` triggers, **Then** reconnection is attempted with exponential backoff (configurable via `BleReconnectPolicy`).
3. **Given** reconnection is rate-limited (>5 attempts in 30s), **When** the limit is hit, **Then** reconnection pauses and the callback is notified.
4. **Given** a `ToRadio` packet is ready to send, **When** `sendToRadio(bytes)` is called, **Then** the payload is written to the `ToRadio` characteristic with retry.
5. **Given** the connection is active, **When** `FromRadio` notifications arrive, **Then** they are forwarded to the `RadioTransportCallback`.
---
### User Story 2 — Stream Framing (TCP/Serial) (Priority: P1)
The `StreamFrameCodec` implements the Meshtastic START1/START2 framing protocol used by both TCP and Serial transports. It encodes outbound payloads into framed packets and decodes inbound byte streams back into complete payloads.
**Why this priority**: TCP and Serial transports rely on correct framing. A framing bug corrupts all communication.
**Independent Test**: Fully testable in isolation — pure function on byte arrays.
**Acceptance Scenarios**:
1. **Given** a payload of N bytes, **When** `frameAndSend()` is called, **Then** the output is `[0x94][0xC3][MSB(N)][LSB(N)][payload]`.
2. **Given** a byte stream with valid framing, **When** each byte is fed to `processInputByte()`, **Then** the complete payload is delivered via `onPacketReceived`.
3. **Given** a corrupted stream (missing START2), **When** the sync is lost, **Then** the state machine resets and logs an error.
4. **Given** a frame with length > 512, **When** the length field is parsed, **Then** the packet is rejected and sync is reset.
5. **Given** concurrent writers, **When** `frameAndSend()` is called from multiple coroutines, **Then** the write mutex prevents interleaved frames.
---
### User Story 3 — MQTT Mesh Bridging (Priority: P2)
The MQTT repository manages connections to MQTT brokers for mesh-to-internet packet relay. It subscribes to topics based on the device's channel configuration and publishes outbound mesh packets as MQTT messages.
**Why this priority**: MQTT enables internet-connected mesh communication. Incorrect topic handling causes message loss.
**Independent Test**: Can be tested with mock `MqttClient`.
**Acceptance Scenarios**:
1. **Given** MQTT is configured on the radio, **When** `connect()` is called, **Then** the MQTT client connects with the configured broker, port, username, and password.
2. **Given** an active MQTT connection, **When** the radio publishes a `MqttClientProxyMessage`, **Then** the message is published to the correct topic.
3. **Given** subscribed topics, **When** an inbound MQTT message arrives, **Then** it is decoded (protobuf or JSON) and forwarded to the radio.
4. **Given** a network disconnection, **When** the MQTT client disconnects, **Then** the state transitions to `Disconnected` and reconnection is attempted.
5. **Given** a JSON-encoded MQTT message, **When** decoded, **Then** `MqttJsonPayload` is constructed with the service envelope.
---
### User Story 4 — Network Monitoring & Service Discovery (Priority: P2)
The network repository exposes a reactive `Flow<Boolean>` for network availability and a `Flow<List<DiscoveredService>>` for mDNS-discovered Meshtastic services.
**Why this priority**: Network state informs MQTT connectivity decisions and TCP transport availability.
**Independent Test**: Android `ConnectivityManager` can be mocked; NSD requires integration test.
**Acceptance Scenarios**:
1. **Given** the device has internet connectivity, **When** `networkAvailable` is collected, **Then** it emits `true`.
2. **Given** connectivity changes, **When** the network state transitions, **Then** `networkAvailable` emits the new state with deduplication.
3. **Given** a Meshtastic device is advertising via mDNS, **When** service discovery is active, **Then** it appears in `resolvedList`.
---
### User Story 5 — TCP Radio Transport (Priority: P2)
The TCP transport connects to a Meshtastic device over WiFi using the stream framing protocol on port 4403. It wraps `TcpTransport` with wake bytes and heartbeat sending.
**Why this priority**: TCP is the secondary transport for desktop and WiFi-connected devices.
**Independent Test**: Can be tested with a loopback TCP server.
**Acceptance Scenarios**:
1. **Given** a Meshtastic device is reachable at `host:4403`, **When** `TcpRadioTransport.connect()` is called, **Then** the transport sends wake bytes and begins stream frame decoding.
2. **Given** an active TCP connection, **When** a `ToRadio` payload is ready, **Then** it is framed and sent via the stream codec.
3. **Given** the TCP socket disconnects, **When** the transport detects the error, **Then** the callback is notified and reconnection can be attempted.
---
### Edge Cases
- What happens when BLE reconnection is attempted while Bluetooth is disabled? `UnmetRequirementException` is classified and surfaced to the callback.
- What happens when `StreamFrameCodec` receives a zero-length packet? It delivers an empty `ByteArray` via `onPacketReceived`.
- What happens when MQTT subscription fails? The error is logged and the connection state remains active (best-effort).
- What happens when serial USB device is disconnected during write? `IOException` is caught and reported as a transport error.
## Architecture
### Key Components
| Component | File | Purpose |
|-----------|------|---------|
| `BleRadioTransport` | `radio/BleRadioTransport.kt` (506 LOC) | BLE-based radio transport with reconnection |
| `BleReconnectPolicy` | `radio/BleReconnectPolicy.kt` | Configurable reconnection backoff and rate limiting |
| `StreamTransport` | `radio/StreamTransport.kt` | Base class for stream-framed transports (TCP, Serial) |
| `StreamFrameCodec` | `transport/StreamFrameCodec.kt` (154 LOC) | START1/START2 framing encode/decode |
| `HeartbeatSender` | `transport/HeartbeatSender.kt` | Periodic heartbeat for stream transports |
| `TcpRadioTransport` | `radio/TcpRadioTransport.kt` (jvmAndroidMain) | TCP transport on port 4403 |
| `TcpTransport` | `transport/TcpTransport.kt` (jvmAndroidMain) | Raw TCP socket wrapper |
| `SerialRadioTransport` | `radio/SerialRadioTransport.kt` (androidMain) | USB serial transport |
| `MockRadioTransport` | `radio/MockRadioTransport.kt` | Test/demo transport |
| `NopRadioTransport` | `radio/NopRadioTransport.kt` | No-op transport for unconnected state |
| `BaseRadioTransportFactory` | `radio/BaseRadioTransportFactory.kt` | Factory for creating transport instances |
| `MQTTRepositoryImpl` | `repository/MQTTRepositoryImpl.kt` (312 LOC) | MQTT client lifecycle and topic management |
| `NetworkRepositoryImpl` | `repository/NetworkRepositoryImpl.kt` | Network availability + service discovery flows |
| `NetworkMonitor` | `repository/NetworkMonitor.kt` | Interface for connectivity monitoring |
| `ServiceDiscovery` | `repository/ServiceDiscovery.kt` | Interface for mDNS/NSD discovery |
| `FirmwareReleaseRemoteDataSource` | `FirmwareReleaseRemoteDataSource.kt` | Ktor HTTP fetch for firmware releases |
| `DeviceHardwareRemoteDataSource` | `DeviceHardwareRemoteDataSource.kt` | Ktor HTTP fetch for hardware catalog |
| `HttpClientDefaults` | `HttpClientDefaults.kt` | Ktor client configuration (timeouts, content negotiation) |
| `ApiService` | `service/ApiService.kt` | REST API service abstraction |
| `CoreNetworkModule` | `di/CoreNetworkModule.kt` | Koin DI module with JSON provider |
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST implement `RadioTransport` interface for BLE, TCP, Serial, Mock, and Nop transports.
- **FR-002**: BLE transport MUST scan, connect, discover Meshtastic profile, and observe `FromRadio` notifications.
- **FR-003**: BLE transport MUST implement automatic reconnection with `BleReconnectPolicy` (configurable backoff, rate limiting).
- **FR-004**: System MUST implement `StreamFrameCodec` with START1 (0x94) / START2 (0xC3) / 2-byte length / payload framing.
- **FR-005**: `StreamFrameCodec` MUST reject frames with payload > 512 bytes (`MAX_TO_FROM_RADIO_SIZE`).
- **FR-006**: `StreamFrameCodec.frameAndSend()` MUST be thread-safe via internal mutex.
- **FR-007**: TCP transport MUST connect to port 4403 and send wake bytes before framing begins.
- **FR-008**: System MUST implement `MQTTRepository` with connect, subscribe, publish, disconnect, and connection state tracking.
- **FR-009**: MQTT client MUST subscribe to topic patterns based on radio channel configuration.
- **FR-010**: MQTT client MUST decode both protobuf and JSON-encoded inbound messages.
- **FR-011**: System MUST provide `NetworkMonitor` with reactive connectivity state.
- **FR-012**: System MUST provide `ServiceDiscovery` for mDNS/NSD-based local device discovery.
- **FR-013**: System MUST provide Ktor HTTP client for firmware and hardware catalog API access.
- **FR-014**: `HeartbeatSender` MUST send periodic keep-alive packets on stream transports.
- **FR-015**: Serial transport MUST handle USB device attach/detach events via `UsbBroadcastReceiver`.
- **FR-016**: BLE transport MUST request high connection priority for latency-sensitive operations (firmware update).
### Non-Functional Requirements
- **NFR-001**: All shared transport logic MUST reside in `commonMain` (Constitution §I).
- **NFR-002**: Platform-specific transports (Serial, USB) MUST be in `androidMain`; TCP in `jvmAndroidMain`.
- **NFR-003**: BLE reconnect backoff MUST not exceed configured maximum delay.
- **NFR-004**: MQTT JSON parsing MUST use `safeCatching {}` (Constitution §VII).
- **NFR-005**: HTTP client MUST use lenient JSON with `ignoreUnknownKeys = true`.
- **NFR-006**: Stream codec debug output MUST use line-buffered `Logger.d` for readability.
## Source-Set Impact
| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | 23 files (~3,200 LOC) | Transport interfaces, BLE transport, stream codec, MQTT, network, HTTP |
| `commonTest` | 7 files (~600 LOC) | BLE transport, reconnect policy, stream codec, MQTT tests |
| `jvmAndroidMain` | 2 files (~300 LOC) | TCP transport + socket |
| `jvmMain` | 3 files (~250 LOC) | JVM network monitor, service discovery, serial transport |
| `jvmTest` | 1 file (~100 LOC) | JVM service discovery test |
| `androidMain` | 14 files (~1,100 LOC) | USB/Serial, Android network monitor, NSD, connectivity |
## Privacy Assessment
- [x] No PII logged — device addresses anonymized, MQTT credentials not logged
- [x] MQTT topic patterns do not contain user identifiers
- [x] Proto submodule not modified
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: `StreamFrameCodec` round-trips: frame → parse produces the original payload for any size 0512.
- **SC-002**: BLE transport successfully connects, observes, and sends data with mock `BleConnection` in tests.
- **SC-003**: `BleReconnectPolicy` correctly backs off after disconnection — delays increase up to the configured maximum.
- **SC-004**: MQTT repository connects to broker and subscribes to configured topics within 5s.
- **SC-005**: BLE reconnect crash test verifies no crash on rapid connect/disconnect cycles.
- **SC-006**: Network monitor emits correct state transitions on connectivity changes.
- **SC-007**: All 8 existing test files pass with `allTests` target.
- **SC-008**: Stream codec handles all edge cases: zero-length packets, oversized frames, lost sync recovery.
- **SC-009**: HTTP client fetches firmware release JSON and parses it correctly.
## Assumptions
- BLE is the primary transport (~90% of connections); TCP and Serial are secondary.
- `core/ble` provides `BleConnection`, `BleScanner`, and `BleConnectionFactory` via Koin injection.
- MQTT client library is wrapped behind `MqttClient` interface from the `:mqtt` module.
- USB serial uses `usb-serial-for-android` library (Android-only).
- TCP port 4403 is the standard Meshtastic TCP service port.
- mDNS service type for Meshtastic is `_meshtastic._tcp.local.`.

View File

@@ -1,199 +0,0 @@
# Tasks: Core Network & Radio Transport
**Spec**: [spec.md](spec.md) | **Plan**: [plan.md](plan.md)
**Status**: Migrated — all existing tasks marked complete. Gap tasks marked incomplete.
**Task Prefix**: `NET-T`
---
## Phase 1 — Transport Interfaces & Stream Codec
### NET-T001: Koin DI module + JSON provider [x]
- **File**: `di/CoreNetworkModule.kt`
- Provides lenient `Json` instance with `ignoreUnknownKeys`, `coerceInputValues`.
- `@ComponentScan("org.meshtastic.core.network")` for auto-discovery.
- **Test**: Module loads without error.
### NET-T002: StreamFrameCodec [x]
- **File**: `transport/StreamFrameCodec.kt` (~154 LOC)
- START1 (0x94) / START2 (0xC3) / 2-byte-length / payload framing.
- Byte-at-a-time state machine for decode; mutex-protected `frameAndSend()` for encode.
- Max payload 512 bytes; sync recovery on corruption.
- **Test**: `StreamFrameCodecTest.kt` — covers round-trip, oversized, zero-length, lost sync.
### NET-T003: HeartbeatSender [x]
- **File**: `transport/HeartbeatSender.kt`
- Periodic keep-alive for stream transports (TCP, Serial).
- Configurable interval.
- **Test**: Verified via transport integration.
### NET-T004: NopRadioTransport [x]
- **File**: `radio/NopRadioTransport.kt`
- No-op implementation for unconnected state.
- **Test**: Trivial — no behavior to test.
### NET-T005: StreamTransport base class [x]
- **File**: `radio/StreamTransport.kt`
- Shared base for TCP and Serial transports.
- Integrates `StreamFrameCodec` and `HeartbeatSender`.
- **Test**: Verified via `StreamTransportTest.kt`.
### NET-T006: BaseRadioTransportFactory [x]
- **File**: `radio/BaseRadioTransportFactory.kt`
- Factory for creating transport instances by type (BLE, TCP, Serial, Mock).
- **Test**: Verified via integration.
---
## Phase 2 — BLE Radio Transport
### NET-T007: BleRadioTransport [x]
- **File**: `radio/BleRadioTransport.kt` (~506 LOC)
- Complete BLE transport: scan → connect → profile discovery → FromRadio observation → ToRadio write.
- Integrates `BleConnection`, `BleScanner`, `BleConnectionFactory`, `MeshtasticRadioProfile`.
- Handles disconnect detection and callback notification.
- High connection priority request for firmware updates.
- **Test**: `BleRadioTransportTest.kt` — covers connect, send, receive, disconnect.
### NET-T008: BleReconnectPolicy [x]
- **File**: `radio/BleReconnectPolicy.kt`
- Configurable exponential backoff for reconnection.
- Rate limiting: >5 attempts in 30s triggers pause.
- **Test**: `BleReconnectPolicyTest.kt`, `ReconnectBackoffTest.kt` — covers backoff progression, rate limiting.
### NET-T009: BLE reconnect crash resilience [x]
- **File**: (tested within `BleRadioTransport`)
- Rapid connect/disconnect cycles must not crash.
- Mutex protection for concurrent state access.
- **Test**: `BleRadioTransportReconnectCrashTest.kt` — rapid cycle stress test.
---
## Phase 3 — Secondary Transports
### NET-T010: TcpRadioTransport + TcpTransport [x]
- **Files**: `jvmAndroidMain/.../TcpRadioTransport.kt`, `TcpTransport.kt`
- TCP connection on port 4403 with wake bytes.
- Stream-framed encoding/decoding via `StreamFrameCodec`.
- **Test**: Verified via manual integration (no automated test).
### NET-T011: SerialRadioTransport (Android) [x]
- **Files**: `androidMain/.../SerialRadioTransport.kt`, `SerialConnection*.kt`, `UsbManager.kt`, `UsbBroadcastReceiver.kt`
- USB serial transport using `usb-serial-for-android`.
- Handles USB attach/detach events, probe table, serial parameters.
- **Test**: Verified via Android device testing (no automated test).
### NET-T012: USB Repository (Android) [x]
- **File**: `androidMain/.../UsbRepository.kt`
- USB device enumeration and permission management.
- **Test**: Verified via device testing.
### NET-T013: MockRadioTransport [x]
- **File**: `radio/MockRadioTransport.kt`
- Controllable transport for tests and demo mode.
- **Test**: Used as a dependency in other tests.
### NET-T014: Android transport factory [x]
- **File**: `androidMain/.../AndroidRadioTransportFactory.kt`
- Extends `BaseRadioTransportFactory` with Serial and TCP support.
- **Test**: Verified via app integration.
---
## Phase 4 — MQTT & Network Infrastructure
### NET-T015: MQTTRepositoryImpl [x]
- **File**: `repository/MQTTRepositoryImpl.kt` (~312 LOC)
- MQTT client lifecycle: connect, subscribe (topic patterns from channel config), publish, disconnect.
- Decodes protobuf and JSON inbound messages.
- Connection state as `StateFlow`.
- Semaphore-based concurrency control.
- **Test**: `MQTTRepositoryImplTest.kt` — covers basic lifecycle.
### NET-T016: NetworkRepositoryImpl [x]
- **File**: `repository/NetworkRepositoryImpl.kt` (~62 LOC)
- Exposes `networkAvailable: Flow<Boolean>` and `resolvedList: Flow<List<DiscoveredService>>`.
- `shareIn` with `WhileSubscribed` for lifecycle awareness.
- **Test**: Verified via integration.
### NET-T017: Network monitoring platform implementations [x]
- **Files**: `androidMain/.../AndroidNetworkMonitor.kt`, `ConnectivityManager.kt`, `jvmMain/.../JvmNetworkMonitor.kt`
- Android: `ConnectivityManager` callback; JVM: periodic polling.
- **Test**: Verified via platform integration.
### NET-T018: Service discovery (mDNS/NSD) [x]
- **Files**: `repository/ServiceDiscovery.kt`, `DiscoveredService.kt`, `androidMain/.../AndroidServiceDiscovery.kt`, `NsdManager.kt`, `jvmMain/.../JvmServiceDiscovery.kt`
- Android: NSD API; JVM: JmDNS.
- **Test**: `JvmServiceDiscoveryTest.kt` — JVM-only integration test.
### NET-T019: HTTP client + remote data sources [x]
- **Files**: `HttpClientDefaults.kt`, `KermitHttpLogger.kt`, `FirmwareReleaseRemoteDataSource.kt`, `DeviceHardwareRemoteDataSource.kt`, `service/ApiService.kt`
- Ktor client with lenient JSON, content negotiation, timeouts.
- Fetches firmware releases and hardware catalog from GitHub API.
- **Test**: Verified via firmware update feature integration.
### NET-T020: MQTT logging bridge [x]
- **File**: `repository/KermitMqttLogger.kt`
- Bridges MQTT client logging to Kermit.
- **Test**: Verified via MQTT operation logging.
### NET-T021: Network constants [x]
- **File**: `repository/NetworkConstants.kt`
- MQTT, TCP, and network-related constants.
- **Test**: Used as dependencies throughout.
---
## Gap Tasks (Incomplete)
### NET-T022: Add TcpRadioTransport unit tests [ ]
- **File to create**: `jvmAndroidTest/.../TcpRadioTransportTest.kt`
- Test with loopback TCP server: connect, frame, send, receive, disconnect.
- **Priority**: Medium
### NET-T023: Add SerialRadioTransport instrumented tests [ ]
- **File to create**: `androidDeviceTest/.../SerialRadioTransportTest.kt`
- Test USB attach/detach event handling; serial parameter configuration.
- **Priority**: Medium
### NET-T024: Expand MQTT test coverage [x]
- **File to extend**: `commonTest/.../MQTTRepositoryImplTest.kt`
- Add tests: topic pattern construction, JSON decode, protobuf decode, reconnect, subscription failure.
- **Priority**: Medium
### NET-T025: Add HeartbeatSender unit test [x]
- **File to create**: `commonTest/.../HeartbeatSenderTest.kt`
- Test periodic interval, cancellation, edge cases.
- **Priority**: Low
### NET-T026: Add HTTP remote data source tests [ ]
- **File to create**: `commonTest/.../FirmwareReleaseRemoteDataSourceTest.kt`
- Test with Ktor `MockEngine`: success, error, malformed JSON.
- **Priority**: Low

View File

@@ -1,91 +0,0 @@
# Implementation Plan: Core Database (Room KMP Persistence)
**Branch**: `015-core-database` | **Date**: 2026-07-27 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/015-core-database/spec.md`
**Status**: Migrated — all implementation complete, plan reverse-engineered from existing code.
## Summary
Core Database defines the Room KMP schema, per-device database management with LRU eviction, 35 auto-migrations, 11 entities, 7 DAOs, and cross-platform database builders. The `DatabaseManager` (301 LOC) is the central piece — it manages a cache of open databases, tracks usage via DataStore, and enforces configurable limits.
## Technical Context
**Language/Version**: Kotlin 2.3+ targeting JDK 21
**Primary Dependencies**: Room KMP (`androidx.room3`), DataStore KMP, Okio, kotlinx.coroutines, Kermit
**Testing**: 3 commonTest files, 2 androidDeviceTest files, 1 androidHostTest file; ~600 LOC total
**Target Platform**: Android, Desktop (JVM), iOS
**Constraints**: Schema must auto-migrate; `dropAllTables = false` for destructive fallback; all entities in `commonMain`
**Scale/Scope**: 23 commonMain files (~2,800 LOC), 4 platform files (~200 LOC), 6 test files (~600 LOC)
## Constitution Check
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Kotlin Multiplatform Core | ✅ PASS | All entities, DAOs, and database definitions in `commonMain`. Platform-specific builders via expect/actual. |
| II. Zero Lint Tolerance | ✅ PASS | `detekt-baseline.xml` present. `@Suppress("TooManyFunctions")` on `DatabaseManager`. |
| VII. Coroutine Safety | ✅ PASS | Mutex serialization for DB switching. `limitedParallelism(4)` for `withDb()`. `CancellationException` propagated. |
| IX. Branch & Scope Hygiene | ✅ PASS | Module scoped to `org.meshtastic.core.database`. |
**Gate Result**: ✅ All applicable principles satisfied.
## Project Structure
```
core/database/src/
├── commonMain/kotlin/org/meshtastic/core/database/
│ ├── di/CoreDatabaseModule.kt
│ ├── MeshtasticDatabase.kt # 141 LOC — schema definition, 35 auto-migrations
│ ├── DatabaseManager.kt # 301 LOC — per-device management, LRU eviction
│ ├── DatabaseProvider.kt # Interface
│ ├── DatabaseBuilder.kt # expect declaration
│ ├── DatabaseConstants.kt # Naming, limits
│ ├── Converters.kt # Type converters
│ ├── MeshtasticDatabaseConstructor.kt
│ ├── entity/ (8 entity files)
│ └── dao/ (7 DAO files)
├── commonTest/ (3 files)
├── androidDeviceTest/ (2 files)
├── androidHostTest/ (1 file — MigrationTest)
├── androidMain/ (2 files — builder, DI)
├── jvmMain/ (1 file — builder)
├── iosMain/ (1 file — builder)
└── schemas/ (exported Room schemas for migration validation)
```
## Implementation Phases
### Phase 1 — Schema & Entities (Complete)
11 Room entities covering nodes, messages, logs, quick chat, firmware, hardware, traceroutes. Type converters for proto ↔ blob and ByteString ↔ ByteArray.
### Phase 2 — DAOs (Complete)
7 DAOs with reactive `Flow`-based queries, paging support (`PagingSourceDaoReturnTypeConverter`), and bulk operations.
### Phase 3 — Database Manager (Complete)
`DatabaseManager` (301 LOC): per-device database cache, `switchActiveDatabase()` with mutex serialization, `withDb()` with retry, LRU eviction, legacy cleanup, DataStore-backed preferences. Platform-specific `DatabaseBuilder` expect/actual.
## Technical Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| ORM | Room KMP (`androidx.room3`) | Official Google ORM with KMP support; mature migration system |
| Per-device isolation | Separate DB files per BLE address | Prevents data cross-contamination; enables clean device removal |
| Cache strategy | In-memory `mutableMapOf` with LRU eviction | Fast access; bounded storage via configurable limit |
| DB switching | Emit new DB before closing old | Prevents "connection pool closed" races with active collectors |
| Retry on closed pool | Single retry in `withDb()` | Handles the narrow race between DB switch and in-flight queries |
| Concurrency limit | `limitedParallelism(4)` for `withDb()` | Prevents SQLite connection pool exhaustion |
| Migration strategy | Auto-migration with `exportSchema = true` | Simplest path; schema exports enable validation testing |
| Destructive fallback | `dropAllTables = false` | Preserves data in unaffected tables during emergency fallback |
## Gaps Identified
| Gap | Severity | Recommendation |
|-----|----------|----------------|
| No commonTest for `Converters` | ⚠️ Low | Add round-trip tests for proto ↔ blob, ByteString ↔ ByteArray |
| Only 2 of 7 DAOs have unit tests | ⚠️ Medium | Add tests for `MeshLogDao`, `QuickChatActionDao`, `DeviceHardwareDao`, `FirmwareReleaseDao`, `TracerouteNodePositionDao` |
| Migration test is Android-only | ⚠️ Low | Room KMP migration testing is currently Android-only; acceptable limitation |
| `DatabaseManager.close()` uses `runCatching` not `safeCatching` | ⚠️ Low | Minor constitution deviation; acceptable in cleanup paths |
| No test for concurrent `withDb()` retry behavior | ⚠️ Medium | Add test that simulates DB switch during query execution |

View File

@@ -1,201 +0,0 @@
# Feature Specification: Core Database (Room KMP Persistence)
**Feature Branch**: `015-core-database`
**Created**: 2026-07-27
**Status**: Migrated
**Input**: Brownfield migration — reverse-engineered from existing `core/database` module
## Summary
Core Database provides the Room KMP persistence layer for Meshtastic-Android. It defines the `MeshtasticDatabase` schema (11 entities, 7 DAOs, 35 auto-migrations from v3→v38), a `DatabaseManager` with per-device database instances and LRU eviction, per-platform `DatabaseBuilder` implementations, and type converters. The module enables per-device data isolation — each connected Meshtastic device gets its own Room database file — with configurable cache limits and automatic eviction of least-recently-used database files.
## Goals
1. **Per-device persistence** — maintain separate Room database files per Meshtastic device address for data isolation.
2. **Schema evolution** — support forward migration via Room auto-migrations across 35+ schema versions.
3. **LRU eviction** — automatically close and delete least-recently-used database files when the cache limit is exceeded.
4. **Cross-platform** — provide `DatabaseBuilder` implementations for Android, JVM/Desktop, and iOS via expect/actual.
5. **Reactive access** — expose the current database as `StateFlow<MeshtasticDatabase>` for reactive consumers.
## Non-Goals
- Business logic for reading/writing data — handled by `core/data` repositories.
- Domain model definitions — handled by `core/model`.
- Query logic beyond DAO definitions — complex queries are in the repository layer.
- Database encryption — not currently implemented.
## User Scenarios & Testing *(mandatory)*
### User Story 1 — Per-Device Database Switching (Priority: P1)
When the user connects to a different Meshtastic device, the `DatabaseManager` switches the active database to the one associated with the new device's address. A new database is created if none exists for that address.
**Why this priority**: Per-device isolation prevents data cross-contamination between devices.
**Independent Test**: Can be tested by switching addresses and verifying different databases are active.
**Acceptance Scenarios**:
1. **Given** the app connects to device with address "AA:BB:CC", **When** `switchActiveDatabase("AA:BB:CC")` is called, **Then** a database named `meshtastic_AA_BB_CC` is opened (or created).
2. **Given** the active DB is device A, **When** switching to device B, **Then** `currentDb` emits the new database and the previous is not closed synchronously (race-safe).
3. **Given** the same address is requested twice, **When** `switchActiveDatabase` is called again, **Then** it is a no-op (fast path).
4. **Given** a null/blank address, **When** `switchActiveDatabase(null)` is called, **Then** the default database is used.
---
### User Story 2 — LRU Cache Eviction (Priority: P2)
The database manager enforces a configurable cache limit. When the number of per-device databases exceeds the limit, the least-recently-used database files are closed and deleted.
**Why this priority**: Without eviction, device storage fills up as users connect to many different nodes.
**Independent Test**: Testable by creating N databases, setting limit to M < N, and verifying eviction.
**Acceptance Scenarios**:
1. **Given** the cache limit is 5 and 6 databases exist, **When** eviction runs, **Then** the least-recently-used database is closed and its file is deleted.
2. **Given** the active database would be evicted by LRU order, **When** eviction runs, **Then** the active database is protected from eviction.
3. **Given** the cache limit is changed from 5 to 3, **When** `setCacheLimit(3)` is called, **Then** eviction runs asynchronously and removes excess databases.
4. **Given** the cache limit is set to a value outside bounds, **When** clamped, **Then** it is constrained to `[MIN_CACHE_LIMIT, MAX_CACHE_LIMIT]`.
---
### User Story 3 — Schema Migration (Priority: P1)
The Room database supports forward migration from schema version 3 through 38 using auto-migrations. Special migrations handle table deletions and column removals.
**Why this priority**: Schema migration failures cause data loss. The 35-version migration chain must be reliable.
**Independent Test**: Android-only instrumented test (`MigrationTest`).
**Acceptance Scenarios**:
1. **Given** a database at schema version N (3 ≤ N < 38), **When** the app opens it, **Then** Room auto-migrates to version 38 without data loss.
2. **Given** auto-migration from v12→v13, **When** the migration runs, **Then** the legacy `NodeInfo` and `MyNodeInfo` tables are deleted.
3. **Given** auto-migration from v29→v30, **When** the migration runs, **Then** the `reply_id` column is removed from `packet`.
4. **Given** a destructive migration is required, **When** `fallbackToDestructiveMigration(dropAllTables = false)` is configured, **Then** only the affected tables are recreated.
---
### User Story 4 — Database Access via withDb() (Priority: P1)
Consumers access the active database through `DatabaseManager.withDb()`, which provides the current database instance. It tolerates connection-pool-closed races during database switching by retrying once.
**Why this priority**: `withDb()` is the single entry point for all database access. Race safety is critical.
**Independent Test**: Can be tested by simulating a database switch during a `withDb()` call.
**Acceptance Scenarios**:
1. **Given** an active database, **When** `withDb { db.nodeInfoDao().getAll() }` is called, **Then** the query executes on the current database.
2. **Given** a database switch occurs between capturing `db` and executing the query, **When** "Connection pool is closed" is thrown, **Then** `withDb` retries once with the new database instance.
3. **Given** no database is active (`_currentDb` is null), **When** `withDb` is called, **Then** it returns `null` immediately.
4. **Given** concurrent `withDb` calls, **When** executing, **Then** parallelism is limited to 4 via `limitedIo` dispatcher.
---
### Edge Cases
- What happens when deleting a database file fails (permission error)? The error is logged but does not crash; `runCatching` is used for best-effort cleanup.
- What happens when a legacy database exists after migration? `cleanupLegacyDbIfNeeded` deletes it on first switch, then marks cleanup as complete via DataStore.
- What happens when `hasDatabaseFor()` checks for a non-existent address? It checks both `dbName` and `dbName.db` file paths (platform-agnostic).
- What happens when the database directory doesn't exist? `listExistingDbNames()` returns an empty list.
## Architecture
### Key Components
| Component | File | Purpose |
|-----------|------|---------|
| `MeshtasticDatabase` | `MeshtasticDatabase.kt` (141 LOC) | Room database definition: 11 entities, 7 DAOs, 35 auto-migrations |
| `DatabaseManager` | `DatabaseManager.kt` (301 LOC) | Per-device DB management: switch, LRU eviction, withDb(), legacy cleanup |
| `DatabaseProvider` | `DatabaseProvider.kt` | Interface for database access (currentDb, withDb) |
| `DatabaseBuilder` | `DatabaseBuilder.kt` (expect/actual) | Platform-specific Room database builder |
| `DatabaseConstants` | `DatabaseConstants.kt` | DB prefix, limits, legacy name constants |
| `Converters` | `Converters.kt` | Room type converters (ByteString ↔ ByteArray, proto ↔ blob) |
| `MeshtasticDatabaseConstructor` | `MeshtasticDatabaseConstructor.kt` | Room KMP database constructor |
| `CoreDatabaseModule` | `di/CoreDatabaseModule.kt` | Koin DI module |
| **Entities** | | |
| `NodeEntity` | `entity/NodeEntity.kt` | Mesh node persistence |
| `MyNodeEntity` | `entity/MyNodeEntity.kt` | Local node identity |
| `Packet` | `entity/Packet.kt` | Message/packet persistence |
| `MeshLog` | `entity/MeshLog.kt` | Debug mesh log entries |
| `QuickChatAction` | `entity/QuickChatAction.kt` | Quick chat shortcuts |
| `FirmwareReleaseEntity` | `entity/FirmwareReleaseEntity.kt` | Cached firmware releases |
| `DeviceHardwareEntity` | `entity/DeviceHardwareEntity.kt` | Cached hardware catalog |
| `TracerouteNodePositionEntity` | `entity/TracerouteNodePositionEntity.kt` | Traceroute position snapshots |
| **DAOs** | | |
| `NodeInfoDao` | `dao/NodeInfoDao.kt` | Node CRUD with reactive queries |
| `PacketDao` | `dao/PacketDao.kt` | Message queries with paging support |
| `MeshLogDao` | `dao/MeshLogDao.kt` | Log insertion and paging |
| `QuickChatActionDao` | `dao/QuickChatActionDao.kt` | Quick chat CRUD |
| `DeviceHardwareDao` | `dao/DeviceHardwareDao.kt` | Hardware catalog CRUD |
| `FirmwareReleaseDao` | `dao/FirmwareReleaseDao.kt` | Firmware release CRUD |
| `TracerouteNodePositionDao` | `dao/TracerouteNodePositionDao.kt` | Traceroute position CRUD |
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST define a Room database with 11 entities and 7 DAOs.
- **FR-002**: System MUST support auto-migration from schema version 3 to 38 (35 migration steps).
- **FR-003**: System MUST provide per-device database instances keyed by Bluetooth address.
- **FR-004**: System MUST implement LRU eviction with configurable cache limit (min/max bounds).
- **FR-005**: System MUST expose the active database as `StateFlow<MeshtasticDatabase>`.
- **FR-006**: System MUST provide `withDb()` with connection-pool-closed retry logic.
- **FR-007**: System MUST clean up legacy database files on first use after migration.
- **FR-008**: System MUST provide platform-specific `DatabaseBuilder` via expect/actual.
- **FR-009**: System MUST provide `hasDatabaseFor(address)` to check if a device has persisted data.
- **FR-010**: System MUST track database last-used timestamps via DataStore preferences.
- **FR-011**: System MUST protect the active database from LRU eviction.
- **FR-012**: System MUST configure Room with `fallbackToDestructiveMigration(dropAllTables = false)`.
- **FR-013**: System MUST set query coroutine context to `ioDispatcher` for all Room operations.
- **FR-014**: System MUST support paging via `PagingSourceDaoReturnTypeConverter`.
### Non-Functional Requirements
- **NFR-001**: All entity, DAO, and database definitions MUST reside in `commonMain` (Constitution §I).
- **NFR-002**: Database switching MUST be serialized via `Mutex` to prevent race conditions.
- **NFR-003**: `withDb()` parallelism MUST be limited to 4 concurrent operations via `limitedParallelism`.
- **NFR-004**: Database file eviction MUST be best-effort — failures are logged, not propagated.
- **NFR-005**: Legacy database cleanup MUST be idempotent (DataStore flag prevents repeat cleanup).
## Source-Set Impact
| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | 23 files (~2,800 LOC) | Database, entities, DAOs, manager, converters |
| `commonTest` | 3 files (~300 LOC) | NodeInfoDao, PacketDao, eviction tests |
| `androidDeviceTest` | 2 files (~200 LOC) | Full DB test, legacy cleanup test |
| `androidHostTest` | 1 file (~100 LOC) | Migration test |
| `androidMain` | 2 files (~100 LOC) | Android DatabaseBuilder, Android DI module |
| `jvmMain` | 1 file (~50 LOC) | JVM DatabaseBuilder |
| `iosMain` | 1 file (~50 LOC) | iOS DatabaseBuilder |
## Privacy Assessment
- [x] Database files contain mesh node data (addresses, positions) — stored locally only
- [x] Database file names derived from Bluetooth MAC are anonymized in logs
- [x] No database exports or cloud sync
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: All 35 auto-migrations execute without data loss from v3→v38.
- **SC-002**: `DatabaseManager` correctly switches between 3+ per-device databases.
- **SC-003**: LRU eviction removes exactly the right number of databases when cache limit is exceeded.
- **SC-004**: Active database is never evicted regardless of LRU order.
- **SC-005**: `withDb()` retries successfully after a connection-pool-closed race.
- **SC-006**: All 6 existing test files pass.
- **SC-007**: Database builder compiles and produces valid Room instances on all 3 platforms.
## Assumptions
- Room KMP (`androidx.room3`) is the persistence library.
- Per-device database naming convention: `meshtastic_{sanitized_address}`.
- Default cache limit is defined in `DatabaseConstants.DEFAULT_CACHE_LIMIT`.
- `ioDispatcher` is the coroutine context for all Room query execution.
- DataStore preferences are used for cache limit and legacy cleanup tracking.
- Schema exports are enabled (`exportSchema = true`) for migration validation.

View File

@@ -1,151 +0,0 @@
# Tasks: Core Database (Room KMP Persistence)
**Spec**: [spec.md](spec.md) | **Plan**: [plan.md](plan.md)
**Status**: Migrated — all existing tasks marked complete. Gap tasks marked incomplete.
**Task Prefix**: `DB-T`
---
## Phase 1 — Schema & Entities
### DB-T001: Room database definition [x]
- **File**: `MeshtasticDatabase.kt` (~141 LOC)
- 11 entities, 7 abstract DAO accessors.
- 35 auto-migrations (v3→v38) with 4 special migration specs.
- `@TypeConverters(Converters::class)`, `@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter)`.
- `configureCommon()` extension: `fallbackToDestructiveMigration(false)` + `setQueryCoroutineContext(ioDispatcher)`.
- **Test**: `MigrationTest.kt` (androidHostTest).
### DB-T002: Node entities [x]
- **Files**: `entity/NodeEntity.kt`, `entity/MyNodeEntity.kt`
- `NodeEntity`: num, user, position, metrics, flags, metadata fields.
- `MyNodeEntity`: local node identity.
- **Test**: `CommonNodeInfoDaoTest.kt`.
### DB-T003: Message entity [x]
- **File**: `entity/Packet.kt`
- Message persistence: data, port_num, contact_key, time, status, reactions.
- **Test**: `CommonPacketDaoTest.kt`.
### DB-T004: Support entities [x]
- **Files**: `entity/MeshLog.kt`, `entity/QuickChatAction.kt`, `entity/FirmwareReleaseEntity.kt`, `entity/DeviceHardwareEntity.kt`, `entity/TracerouteNodePositionEntity.kt`
- Debug logs, quick chat shortcuts, firmware/hardware catalogs, traceroute positions.
- **Test**: Partial — verified via integration.
### DB-T005: Type converters [x]
- **File**: `Converters.kt`
- Proto message ↔ `ByteArray` (blob), `ByteString``ByteArray`, enum conversions.
- **Test**: Verified via DAO round-trips.
### DB-T006: Database constants [x]
- **File**: `DatabaseConstants.kt`
- `DB_PREFIX`, `DEFAULT_DB_NAME`, `LEGACY_DB_NAME`, `DEFAULT_CACHE_LIMIT`, `MIN_CACHE_LIMIT`, `MAX_CACHE_LIMIT`.
- **Test**: Used as dependency throughout.
---
## Phase 2 — DAOs
### DB-T007: NodeInfoDao [x]
- **File**: `dao/NodeInfoDao.kt`
- CRUD: insert, update, delete, getAll, getByNum, flow queries.
- Reactive `Flow<List<NodeEntity>>` for node list.
- **Test**: `CommonNodeInfoDaoTest.kt`.
### DB-T008: PacketDao [x]
- **File**: `dao/PacketDao.kt`
- Message queries with paging, contact-key filtering, unread counts.
- **Test**: `CommonPacketDaoTest.kt`.
### DB-T009: MeshLogDao [x]
- **File**: `dao/MeshLogDao.kt`
- Log insertion, paging, auto-eviction by count.
- **Test**: Verified via `CommonMeshLogRepositoryTest.kt` in `core/data`.
### DB-T010: Supporting DAOs [x]
- **Files**: `dao/QuickChatActionDao.kt`, `dao/DeviceHardwareDao.kt`, `dao/FirmwareReleaseDao.kt`, `dao/TracerouteNodePositionDao.kt`
- Standard CRUD + reactive queries for each entity.
- **Test**: Verified via corresponding repository tests in `core/data`.
---
## Phase 3 — Database Manager
### DB-T011: DatabaseManager — core lifecycle [x]
- **File**: `DatabaseManager.kt` (~301 LOC)
- Per-device database cache (`mutableMapOf<String, MeshtasticDatabase>`).
- `switchActiveDatabase(address)`: mutex-serialized, emit-before-close pattern.
- `withDb(block)`: limited parallelism (4), retry on connection-pool-closed.
- `currentDb: StateFlow<MeshtasticDatabase>` with `filterNotNull().stateIn()`.
- **Test**: `DatabaseManagerEvictionTest.kt`.
### DB-T012: LRU eviction [x]
- **File**: `DatabaseManager.kt` (within class)
- `enforceCacheLimit(activeDbName)`: sorted by last-used timestamp, evicts excess.
- `selectEvictionVictims()`: excludes active DB and system DBs.
- `closeCachedDatabase()`: close + remove from cache.
- DataStore-tracked `lastUsedKey(dbName)` with file-metadata fallback.
- **Test**: `DatabaseManagerEvictionTest.kt`.
### DB-T013: Legacy database cleanup [x]
- **File**: `DatabaseManager.kt` (within class)
- One-time cleanup of pre-migration `meshtastic_database` file.
- Idempotent via DataStore `legacyCleanedKey` flag.
- **Test**: `DatabaseManagerLegacyCleanupTest.kt` (androidDeviceTest).
### DB-T014: DatabaseProvider interface [x]
- **File**: `DatabaseProvider.kt`
- Exposes `currentDb`, `withDb()`, `hasDatabaseFor()`.
- **Test**: Interface contract verified via `DatabaseManager`.
### DB-T015: Platform-specific DatabaseBuilder [x]
- **Files**: `commonMain/.../DatabaseBuilder.kt` (expect), `androidMain`, `jvmMain`, `iosMain` (actual)
- Android: `Room.databaseBuilder(context, ...)`.
- JVM: `Room.databaseBuilder(databaseFile)`.
- iOS: `Room.databaseBuilder(NSFileManager path)`.
- **Test**: `MeshtasticDatabaseTest.kt` (androidDeviceTest) verifies Android builder.
### DB-T016: Koin DI module [x]
- **File**: `di/CoreDatabaseModule.kt`
- `@ComponentScan("org.meshtastic.core.database")`.
- Provides `@Named("DatabaseDataStore")` DataStore instance.
- **Test**: Module loads without error.
---
## Gap Tasks (Incomplete)
### DB-T017: Add Converters round-trip tests [x]
- **File to create**: `commonTest/.../ConvertersTest.kt`
- Test proto ↔ ByteArray, ByteString ↔ ByteArray round-trips for all converter methods.
- **Priority**: Low
### DB-T018: Add missing DAO tests [x]
- **Files to create**: `commonTest/.../dao/CommonQuickChatActionDaoTest.kt`, `CommonMeshLogDaoTest.kt`, etc.
- Cover CRUD + reactive query behavior for untested DAOs.
- **Priority**: Medium
### DB-T019: Add withDb() concurrent retry test [x]
- **File to create**: `commonTest/.../DatabaseManagerRetryTest.kt`
- Simulate DB switch during active `withDb()` query; verify retry succeeds.
- **Priority**: Medium

View File

@@ -1,80 +0,0 @@
# Implementation Plan: Core Service (Mesh Service Bridge)
**Branch**: `016-core-service` | **Date**: 2026-07-27 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/016-core-service/spec.md`
**Status**: Migrated — all implementation complete, plan reverse-engineered from existing code.
## Summary
Core Service bridges the platform layer with the KMP data layer. The `MeshServiceOrchestrator` (165 LOC) manages the start/stop lifecycle in `commonMain`, while `MeshService` (406 LOC) provides the Android foreground service host. The module is heavily platform-split: 5 commonMain files vs 33 androidMain files.
## Technical Context
**Language/Version**: Kotlin 2.3+ targeting JDK 21
**Primary Dependencies**: Koin 4.2+, kotlinx.coroutines, Kermit, WorkManager (Android), AndroidX Core (Android)
**Testing**: 1 commonTest + 7 androidHostTest files, ~700 LOC
**Target Platform**: Android (primary), Desktop (JVM), iOS (future)
**Constraints**: Orchestrator in `commonMain`; all Android service/worker/receiver code in `androidMain`
**Scale/Scope**: 5 commonMain files (~600 LOC), 33 androidMain files (~4,500 LOC), 8 test files (~700 LOC)
## Constitution Check
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Kotlin Multiplatform Core | ✅ PASS | Orchestrator in `commonMain`. Android code correctly in `androidMain`. |
| II. Zero Lint Tolerance | ✅ PASS | `detekt-baseline.xml` present. `@Suppress("LargeClass")` on `MeshService`. |
| VII. Coroutine Safety | ✅ PASS | SupervisorJob in orchestrator scope. `handledLaunch` for service actions. Detached scope for disconnect. |
| IX. Branch & Scope Hygiene | ✅ PASS | Module scoped to `org.meshtastic.core.service`. |
**Gate Result**: ✅ All applicable principles satisfied.
## Project Structure
```
core/service/src/
├── commonMain/ (5 files — orchestrator, service repo, radio interface, DI)
├── commonTest/ (1 file — MeshServiceOrchestratorTest)
├── androidMain/ (33 files — MeshService, notifications, workers, receivers, etc.)
├── androidHostTest/ (7 files — service, notification, worker, location, file tests)
└── jvmMain/ (2 files — JVM location + file stubs)
```
## Implementation Phases
### Phase 1 — KMP Orchestrator (Complete)
`MeshServiceOrchestrator` with `start()`/`stop()` lifecycle, `ServiceRepositoryImpl` reactive state, `CoreServiceModule` DI.
### Phase 2 — Android Service & Notifications (Complete)
`MeshService` foreground service, AIDL binder, `AndroidNotificationManager`, `MeshServiceNotificationsImpl`, notification channel migration.
### Phase 3 — Workers & Receivers (Complete)
`SendMessageWorker`, `MeshLogCleanupWorker`, `ServiceKeepAliveWorker`. Broadcast receivers: `BootCompleteReceiver`, `ReplyReceiver`, `ReactionReceiver`, `MarkAsReadReceiver`.
## Technical Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Architecture | Orchestrator (common) + Service host (Android) | Enables Desktop/iOS to use the same lifecycle |
| Scope per start | Fresh `CoroutineScope` per `start()` | Clean teardown; no collector accumulation across cycles |
| Action isolation | `handledLaunch` per service action | One failed action doesn't terminate the action collector |
| Disconnect on stop | Detached scope with `SupervisorJob` | Allows disconnect drain without scope cancellation |
| Buffer drain | `resetReceivedBuffer()` on start | Prevents stale packet replay from previous session |
| TAK integration | Reactive on/off via preference Flow | No restart required to toggle TAK server |
| Notification type | `FOREGROUND_SERVICE_CONNECTED_DEVICE` | Required by Android 14+ for BLE services |
| AIDL wrapping | `toRemoteExceptions` | Prevents caller crashes from unhandled exceptions |
| Background workers | WorkManager (Android only) | Reliable scheduling with system-managed lifecycle |
## Gaps Identified
| Gap | Severity | Recommendation |
|-----|----------|----------------|
| `DirectRadioControllerImpl` has no test | ⚠️ Medium | Add test for direct radio control operations |
| `SharedRadioInterfaceService` has no test | ⚠️ Medium | Add test for radio interface delegation |
| Only 1 commonTest for entire module | ⚠️ Medium | Add commonTest for `ServiceRepositoryImpl` state transitions |
| `AndroidServiceRepository` AIDL binding not unit tested | ⚠️ Low | AIDL binding validated via `IMeshServiceContractTest` |
| No test for TAK preference reactive start/stop | ⚠️ Low | Add orchestrator test with TAK pref flow |
| `NotificationChannelMigration` has no test | ⚠️ Low | Add test for channel migration logic |

View File

@@ -1,197 +0,0 @@
# Feature Specification: Core Service (Mesh Service Bridge)
**Feature Branch**: `016-core-service`
**Created**: 2026-07-27
**Status**: Migrated
**Input**: Brownfield migration — reverse-engineered from existing `core/service` module
## Summary
Core Service provides the mesh service lifecycle bridge between the platform layer and the KMP data layer. Its centerpiece is `MeshServiceOrchestrator` — a platform-agnostic orchestrator that extracts the startup wiring previously embedded in Android's `MeshService.onCreate()` into a reusable component usable on Android, Desktop, and iOS. The module also contains the Android foreground `MeshService` (AIDL binder, notifications, location, WorkManager workers), `ServiceRepositoryImpl` (reactive state for connection, errors, packets, service actions), `SharedRadioInterfaceService`, platform-specific location/file/notification services, and background workers (send message, mesh log cleanup, service keep-alive).
## Goals
1. **Platform-agnostic service lifecycle**`MeshServiceOrchestrator` starts/stops the mesh service graph without Android dependencies.
2. **Android foreground service**`MeshService` provides the required foreground notification, AIDL binder, and lifecycle hooks.
3. **Reactive service state**`ServiceRepositoryImpl` exposes connection state, errors, packets, and service actions as KMP-compatible flows.
4. **Background workers** — WorkManager workers for message queuing, log cleanup, and service keep-alive on Android.
5. **Platform services** — location, file, and notification services with platform-specific implementations.
## Non-Goals
- Data layer implementation (repositories, managers) — handled by `core/data`.
- Transport layer (BLE, TCP, Serial) — handled by `core/network`.
- UI for service status — handled by feature modules.
- Push notification routing to individual messages — handled by `core/data` message processor.
## User Scenarios & Testing *(mandatory)*
### User Story 1 — Mesh Service Orchestrator Lifecycle (Priority: P1)
The orchestrator starts the mesh service graph: initializes the database, connects the radio, wires `FromRadio` data to the message processor, and dispatches service actions to the router. It stops by cancelling the scope and disconnecting.
**Why this priority**: The orchestrator is the entry point for the entire mesh service. Without it, nothing runs.
**Independent Test**: Can be tested with mock dependencies for radio, database, message processor.
**Acceptance Scenarios**:
1. **Given** the orchestrator is stopped, **When** `start()` is called, **Then** it initializes the per-device database, connects the radio, and begins processing `receivedData`.
2. **Given** `receivedData` emits bytes, **When** a FromRadio packet arrives, **Then** `messageProcessor.handleFromRadio(bytes, myNodeNum)` is called.
3. **Given** a service action is dispatched, **When** it arrives in `serviceRepository.serviceAction`, **Then** it is routed to `router.actionHandler.onServiceAction()` in a supervised coroutine.
4. **Given** `start()` is called while already running, **When** invoked, **Then** it is a no-op.
5. **Given** the orchestrator is running, **When** `stop()` is called, **Then** the scope is cancelled, the radio is disconnected, and TAK server is stopped.
6. **Given** a stale `receivedData` channel from a previous session, **When** `start()` is called, **Then** `resetReceivedBuffer()` drains the channel first.
---
### User Story 2 — ServiceRepositoryImpl Reactive State (Priority: P1)
`ServiceRepositoryImpl` manages all reactive state flows for the mesh service: connection state, error messages, mesh packets, client notifications, traceroute responses, and the service action channel.
**Why this priority**: All feature modules observe these flows for their UI state.
**Independent Test**: Fully testable — pure Kotlin flows with no platform dependencies.
**Acceptance Scenarios**:
1. **Given** the connection state changes, **When** `setConnectionState(Connected)` is called, **Then** `connectionState` StateFlow emits `Connected`.
2. **Given** an error occurs, **When** `setErrorMessage(msg, severity)` is called, **Then** the error flow emits the message.
3. **Given** a service action is dispatched, **When** `dispatchServiceAction(action)` is called, **Then** the action is received by the orchestrator's collector.
4. **Given** a traceroute response arrives, **When** `setTracerouteResponse(response)` is called, **Then** the response flow emits.
---
### User Story 3 — Android Foreground Service (Priority: P1, Android-only)
`MeshService` is an Android foreground service that hosts the orchestrator. It provides the required persistent notification, handles AIDL binding for external clients, and manages the service lifecycle.
**Why this priority**: Android requires a foreground service for persistent radio connections.
**Independent Test**: `IMeshServiceContractTest` validates the AIDL contract.
**Acceptance Scenarios**:
1. **Given** the service is started, **When** `onCreate()` fires, **Then** the foreground notification is posted and `orchestrator.start()` is called.
2. **Given** the service is destroyed, **When** `onDestroy()` fires, **Then** `orchestrator.stop()` is called and resources are cleaned up.
3. **Given** a client binds via AIDL, **When** `onBind()` is called, **Then** the `IMeshService.Stub` binder is returned.
4. **Given** an AIDL method throws, **When** caught, **Then** it is wrapped as a `RemoteException` via `toRemoteExceptions`.
---
### User Story 4 — Background Workers (Priority: P2, Android-only)
WorkManager workers handle background tasks: `SendMessageWorker` queues messages for reliable delivery, `MeshLogCleanupWorker` periodically prunes old logs, and `ServiceKeepAliveWorker` ensures the service stays alive.
**Why this priority**: Background task reliability directly affects message delivery and storage management.
**Independent Test**: `SendMessageWorkerTest` validates the worker logic.
**Acceptance Scenarios**:
1. **Given** a message is queued, **When** `SendMessageWorker` executes, **Then** the message is sent via `CommandSender` and the result is reported.
2. **Given** mesh logs exceed the retention limit, **When** `MeshLogCleanupWorker` runs, **Then** old logs are pruned.
3. **Given** the service is at risk of being killed, **When** `ServiceKeepAliveWorker` fires, **Then** the service is re-started.
---
### Edge Cases
- What happens when `stop()` is called while `start()` is initializing? The scope is cancelled, and the database/radio initialization coroutine is terminated.
- What happens when `radioInterfaceService.disconnect()` throws during stop? It's caught by `runCatching` in a detached coroutine.
- What happens when `orchestrator.start()` fails to connect the radio? The error is emitted via `radioInterfaceService.connectionError` and handled by the service repository.
- What happens when TAK server is enabled but the preferences change mid-session? The orchestrator starts/stops TAK integration reactively.
## Architecture
### Key Components
| Component | File | Purpose |
|-----------|------|---------|
| `MeshServiceOrchestrator` | `commonMain/.../MeshServiceOrchestrator.kt` (165 LOC) | KMP service lifecycle: start/stop, wire data flows |
| `ServiceRepositoryImpl` | `commonMain/.../ServiceRepositoryImpl.kt` (129 LOC) | Reactive state: connection, errors, packets, actions |
| `SharedRadioInterfaceService` | `commonMain/.../SharedRadioInterfaceService.kt` | Shared radio interface bridge |
| `DirectRadioControllerImpl` | `commonMain/.../DirectRadioControllerImpl.kt` | Direct radio control for KMP hosts |
| `CoreServiceModule` | `commonMain/.../di/CoreServiceModule.kt` | Koin DI: ServiceScope with SupervisorJob |
| `MeshService` | `androidMain/.../MeshService.kt` (406 LOC) | Android foreground service with AIDL binder |
| `AndroidServiceRepository` | `androidMain/.../AndroidServiceRepository.kt` | Extends ServiceRepositoryImpl with AIDL binding |
| `AndroidNotificationManager` | `androidMain/.../AndroidNotificationManager.kt` | Notification channels and builders |
| `MeshServiceNotificationsImpl` | `androidMain/.../MeshServiceNotificationsImpl.kt` | Service notification management |
| `AndroidLocationService` | `androidMain/.../AndroidLocationService.kt` | GPS location provider |
| `AndroidFileService` | `androidMain/.../AndroidFileService.kt` | File I/O for exports |
| `SendMessageWorker` | `androidMain/.../worker/SendMessageWorker.kt` | Background message delivery |
| `MeshLogCleanupWorker` | `androidMain/.../worker/MeshLogCleanupWorker.kt` | Log pruning worker |
| `ServiceKeepAliveWorker` | `androidMain/.../worker/ServiceKeepAliveWorker.kt` | Service persistence worker |
| `ServiceBroadcasts` | `androidMain/.../ServiceBroadcasts.kt` | Android broadcast intents |
| `MeshServiceStarter` | `androidMain/.../MeshServiceStarter.kt` | Service start helper |
| `BootCompleteReceiver` | `androidMain/.../BootCompleteReceiver.kt` | Auto-start on boot |
| `ReplyReceiver` | `androidMain/.../ReplyReceiver.kt` | Notification direct reply |
| `ReactionReceiver` | `androidMain/.../ReactionReceiver.kt` | Notification reaction |
| `MarkAsReadReceiver` | `androidMain/.../MarkAsReadReceiver.kt` | Notification mark-as-read |
| `JvmLocationService` | `jvmMain/.../JvmLocationService.kt` | Desktop location stub |
| `JvmFileService` | `jvmMain/.../JvmFileService.kt` | Desktop file I/O |
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST provide `MeshServiceOrchestrator` with `start()` and `stop()` for platform-agnostic service lifecycle.
- **FR-002**: Orchestrator MUST initialize the per-device database before connecting the radio.
- **FR-003**: Orchestrator MUST drain the `receivedData` channel on each `start()` to prevent stale packet replay.
- **FR-004**: Orchestrator MUST forward `receivedData` to `messageProcessor.handleFromRadio()`.
- **FR-005**: Orchestrator MUST dispatch service actions in supervised coroutines (failure isolation).
- **FR-006**: Orchestrator MUST observe TAK server preference and start/stop integration reactively.
- **FR-007**: System MUST provide `ServiceRepositoryImpl` with StateFlows for connection state, errors, packets, notifications.
- **FR-008**: System MUST provide Android `MeshService` as a foreground service with persistent notification.
- **FR-009**: System MUST provide AIDL binder for external client integration via `IMeshService`.
- **FR-010**: System MUST provide `SendMessageWorker` for reliable background message delivery.
- **FR-011**: System MUST provide `MeshLogCleanupWorker` for periodic log pruning.
- **FR-012**: System MUST provide notification receivers for reply, reaction, and mark-as-read.
- **FR-013**: System MUST provide `BootCompleteReceiver` for auto-start on device boot.
- **FR-014**: System MUST provide `ServiceScope` (SupervisorJob + default dispatcher) via Koin.
### Non-Functional Requirements
- **NFR-001**: `MeshServiceOrchestrator` and `ServiceRepositoryImpl` MUST reside in `commonMain` (Constitution §I).
- **NFR-002**: Android-specific code (Service, Workers, Receivers, Notifications) MUST reside in `androidMain`.
- **NFR-003**: Orchestrator `stop()` MUST disconnect on a detached scope to avoid cancellation of the drain delay.
- **NFR-004**: AIDL exceptions MUST be wrapped via `toRemoteExceptions` to prevent caller crashes.
- **NFR-005**: Service coroutine scope MUST use `SupervisorJob` for failure isolation.
## Source-Set Impact
| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | 5 files (~600 LOC) | Orchestrator, ServiceRepositoryImpl, SharedRadioInterfaceService, DirectRadioControllerImpl, DI |
| `commonTest` | 1 file (~100 LOC) | MeshServiceOrchestratorTest |
| `androidMain` | 33 files (~4,500 LOC) | MeshService, notifications, workers, receivers, location, file, DI |
| `androidHostTest` | 7 files (~600 LOC) | Service, notification, worker, location, file tests |
| `jvmMain` | 2 files (~100 LOC) | JVM location and file stubs |
## Privacy Assessment
- [x] Location data is handled by platform services — not logged or transmitted beyond the radio
- [x] Message content is not logged in service layer
- [x] Notification content uses user-visible names only (no raw node numbers)
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Orchestrator `start()` correctly initializes DB and connects radio in test environment.
- **SC-002**: Orchestrator `stop()` cancels scope and disconnects within 200ms grace window.
- **SC-003**: `receivedData` is drained on `start()` — no stale packets from previous session.
- **SC-004**: Service actions are isolated — one action failure does not terminate the collector.
- **SC-005**: `ServiceRepositoryImpl` correctly emits all state transitions.
- **SC-006**: Android `MeshService` posts foreground notification on API 34+ with correct type.
- **SC-007**: AIDL contract tests pass (`IMeshServiceContractTest`).
- **SC-008**: All 8 existing test files pass.
## Assumptions
- Android is the primary platform; Desktop/iOS use the orchestrator directly.
- `MeshService` requires `FOREGROUND_SERVICE_CONNECTED_DEVICE` permission on Android 14+.
- TAK server integration is optional (gated by `TakPrefs.isTakServerEnabled`).
- AIDL interface (`IMeshService`) is deprecated but still required for backward compatibility.
- `ServiceScope` provided by Koin is shared across the orchestrator's lifetime.

View File

@@ -1,179 +0,0 @@
# Tasks: Core Service (Mesh Service Bridge)
**Spec**: [spec.md](spec.md) | **Plan**: [plan.md](plan.md)
**Status**: Migrated — all existing tasks marked complete. Gap tasks marked incomplete.
**Task Prefix**: `SVC-T`
---
## Phase 1 — KMP Orchestrator
### SVC-T001: CoreServiceModule DI [x]
- **File**: `commonMain/.../di/CoreServiceModule.kt`
- `@ComponentScan("org.meshtastic.core.service")`.
- Provides `@Named("ServiceScope")` with `SupervisorJob + dispatchers.default`.
- **Test**: Module loads without error.
### SVC-T002: MeshServiceOrchestrator [x]
- **File**: `commonMain/.../MeshServiceOrchestrator.kt` (~165 LOC)
- `start()`: drain stale buffer, init DB, connect radio, wire `receivedData``messageProcessor`, wire `serviceAction``router.actionHandler`.
- `stop()`: stop TAK, disconnect on detached scope, cancel orchestrator scope.
- `isRunning`: scope?.isActive.
- TAK server reactive start/stop via `takPrefs.isTakServerEnabled`.
- **Test**: `MeshServiceOrchestratorTest.kt`.
### SVC-T003: ServiceRepositoryImpl [x]
- **File**: `commonMain/.../ServiceRepositoryImpl.kt` (~129 LOC)
- `connectionState: StateFlow<ConnectionState>`, `errorMessage`, `meshPacket`, `clientNotification`, `tracerouteResponse`, `serviceAction: Channel`.
- Platform-agnostic — no Android dependencies.
- **Test**: Verified via orchestrator test and `IMeshServiceContractTest`.
### SVC-T004: SharedRadioInterfaceService [x]
- **File**: `commonMain/.../SharedRadioInterfaceService.kt`
- Shared bridge between orchestrator and platform-specific radio interface.
- **Test**: Verified via integration.
### SVC-T005: DirectRadioControllerImpl [x]
- **File**: `commonMain/.../DirectRadioControllerImpl.kt`
- Direct radio control for KMP hosts (Desktop, iOS) that don't use AIDL.
- **Test**: Verified via Desktop app integration.
---
## Phase 2 — Android Service & Notifications
### SVC-T006: MeshService foreground service [x]
- **File**: `androidMain/.../MeshService.kt` (~406 LOC)
- Android `Service` with `startForeground()` on API 34+.
- AIDL `IMeshService.Stub` binder for external clients.
- Delegates lifecycle to `MeshServiceOrchestrator.start()/stop()`.
- `toRemoteExceptions` wrapper on all AIDL methods.
- **Test**: `IMeshServiceContractTest.kt`.
### SVC-T007: Android notification management [x]
- **Files**: `androidMain/.../AndroidNotificationManager.kt`, `MeshServiceNotificationsImpl.kt`, `NotificationChannels.kt`, `NotificationChannelMigration.kt`
- Notification channels: service, messages, alerts.
- Service notification with connection status.
- Message notifications with direct reply + reaction actions.
- Channel migration from legacy channel IDs.
- **Test**: `AndroidNotificationManagerTest.kt`, `MeshServiceNotificationsImplTest.kt`.
### SVC-T008: MeshServiceStarter + BootCompleteReceiver [x]
- **Files**: `androidMain/.../MeshServiceStarter.kt`, `BootCompleteReceiver.kt`
- `MeshServiceStarter`: helper for starting the foreground service.
- `BootCompleteReceiver`: auto-starts service on device boot.
- **Test**: Verified via Android integration.
### SVC-T009: AndroidServiceRepository [x]
- **File**: `androidMain/.../AndroidServiceRepository.kt`
- Extends `ServiceRepositoryImpl` with AIDL-specific binding logic.
- **Test**: Verified via `IMeshServiceContractTest.kt`.
### SVC-T010: ServiceClient + MeshServiceClient [x]
- **Files**: `androidMain/.../ServiceClient.kt`, `MeshServiceClient.kt`
- Client-side helpers for binding to `MeshService` and calling AIDL methods.
- **Test**: Verified via app integration.
### SVC-T011: ServiceBroadcasts [x]
- **File**: `androidMain/.../ServiceBroadcasts.kt`
- Android broadcast intents for mesh state changes.
- **Test**: `ServiceBroadcastsTest.kt`.
---
## Phase 3 — Workers & Receivers
### SVC-T012: SendMessageWorker [x]
- **File**: `androidMain/.../worker/SendMessageWorker.kt`
- WorkManager worker for reliable background message delivery.
- Retries with backoff on failure.
- **Test**: `SendMessageWorkerTest.kt`.
### SVC-T013: MeshLogCleanupWorker [x]
- **File**: `androidMain/.../worker/MeshLogCleanupWorker.kt`
- Periodic log pruning to prevent storage bloat.
- **Test**: Verified via Android integration.
### SVC-T014: ServiceKeepAliveWorker [x]
- **File**: `androidMain/.../worker/ServiceKeepAliveWorker.kt`
- Periodic worker to ensure service stays alive on aggressive OEMs.
- **Test**: Verified via Android integration.
### SVC-T015: Notification action receivers [x]
- **Files**: `androidMain/.../ReplyReceiver.kt`, `ReactionReceiver.kt`, `MarkAsReadReceiver.kt`
- Direct reply, emoji reaction, and mark-as-read from notification actions.
- **Test**: Verified via Android manual testing.
### SVC-T016: Android location service [x]
- **File**: `androidMain/.../AndroidLocationService.kt`
- GPS location provider for position reporting.
- **Test**: `AndroidLocationServiceTest.kt`.
### SVC-T017: AndroidMeshLocationManager [x]
- **File**: `androidMain/.../AndroidMeshLocationManager.kt`
- Manages mesh-based location updates and sharing.
- **Test**: Verified via integration.
### SVC-T018: AndroidFileService [x]
- **File**: `androidMain/.../AndroidFileService.kt`
- File I/O for data exports (CSV, JSON).
- **Test**: `AndroidFileServiceTest.kt`.
### SVC-T019: AndroidMeshWorkerManager [x]
- **File**: `androidMain/.../AndroidMeshWorkerManager.kt`
- Manages WorkManager worker scheduling and constraints.
- **Test**: Verified via integration.
### SVC-T020: JVM platform stubs [x]
- **Files**: `jvmMain/.../JvmLocationService.kt`, `JvmFileService.kt`
- Desktop stubs for location and file services.
- **Test**: Compilation verified.
---
## Gap Tasks (Incomplete)
### SVC-T021: Add ServiceRepositoryImpl unit tests [x]
- **File to create**: `commonTest/.../ServiceRepositoryImplTest.kt`
- Test all state flow emissions: connection, errors, packets, actions, traceroute.
- **Priority**: Medium
### SVC-T022: Add DirectRadioControllerImpl tests [x]
- **File to create**: `commonTest/.../DirectRadioControllerImplTest.kt`
- Test direct radio control operations (send, request config, disconnect).
- **Priority**: Medium
### SVC-T023: Add SharedRadioInterfaceService tests [ ]
- **File to create**: `commonTest/.../SharedRadioInterfaceServiceTest.kt`
- Test radio interface delegation and receivedData forwarding.
- **Priority**: Medium
### SVC-T024: Add TAK preference reactive test [ ]
- **File to extend**: `commonTest/.../MeshServiceOrchestratorTest.kt`
- Test that TAK integration starts/stops reactively on preference change.
- **Priority**: Low

View File

@@ -1,100 +0,0 @@
# Implementation Plan: Core Model (Domain Models)
**Branch**: `017-core-model` | **Date**: 2026-07-27 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/017-core-model/spec.md`
**Status**: Migrated — all implementation complete, plan reverse-engineered from existing code.
## Summary
Core Model is a pure domain layer with 57 commonMain files (~4,500 LOC) defining all domain types, utility functions, and extensions. It has no external dependencies beyond proto definitions and Okio. The module is the most stable in the codebase — changes are infrequent and typically additive (new fields, new utilities).
## Technical Context
**Language/Version**: Kotlin 2.3+ targeting JDK 21
**Primary Dependencies**: Wire protobuf (`core/proto`), Okio (ByteString), kotlinx.serialization (ByteStringSerializer)
**Testing**: 6 commonTest + 3 androidDeviceTest files, ~600 LOC
**Target Platform**: Android, Desktop (JVM), iOS
**Constraints**: Pure domain — no DI, no coroutines, no persistence, no network
**Scale/Scope**: 57 commonMain files (~4,500 LOC), 5 platform files (~200 LOC), 9 test files (~600 LOC)
## Constitution Check
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Kotlin Multiplatform Core | ✅ PASS | 57 of 57 domain files in `commonMain`. Platform actuals limited to DateTime + Random. |
| II. Zero Lint Tolerance | ✅ PASS | `detekt-baseline.xml` present. `@Suppress("MagicNumber")` on `Node`. |
| VII. Coroutine Safety | N/A | No coroutines in this module. |
| IX. Branch & Scope Hygiene | ✅ PASS | Module scoped to `org.meshtastic.core.model`. |
**Gate Result**: ✅ All applicable principles satisfied.
## Project Structure
```
core/model/src/
├── commonMain/kotlin/org/meshtastic/core/model/
│ ├── Node.kt (231 LOC) # Primary domain model
│ ├── DataPacket.kt # Mesh packet representation
│ ├── Message.kt # Chat message model
│ ├── Contact.kt # Contact representation
│ ├── Channel.kt # Channel config model
│ ├── DeviceVersion.kt # Firmware version parser
│ ├── Capabilities.kt # Feature capability flags
│ ├── ConnectionState.kt # Connection state enum
│ ├── SessionStatus.kt # Remote admin session status
│ ├── NodeSortOption.kt # Sort options
│ ├── RadioController.kt # Radio control interface
│ ├── ... (15+ more domain types)
│ ├── service/
│ │ ├── ServiceAction.kt # Service action sealed class
│ │ └── TracerouteResponse.kt # Traceroute result model
│ └── util/
│ ├── ChannelSet.kt # URL encode/decode
│ ├── MeshDataMapper.kt # Proto → domain mapping
│ ├── TimeUtils.kt # Time formatting
│ ├── Extensions.kt # General extensions
│ ├── ... (15+ more utilities)
├── commonTest/ (6 files)
├── androidDeviceTest/ (3 files)
├── jvmAndroidMain/ (2 files — DateTime, Random)
├── androidMain/ (2 files — Uri, TimeZone)
└── iosMain/ (1 file — noop)
```
## Implementation Phases
### Phase 1 — Domain Models (Complete)
Core domain types: `Node`, `DataPacket`, `Message`, `Contact`, `Channel`, `ConnectionState`, `DeviceVersion`, `Capabilities`, `SessionStatus`, plus ~15 supporting types.
### Phase 2 — Utilities & Extensions (Complete)
25+ utility files: time/date formatting, distance/coordinate calculations, URL encoding, proto extensions, byte string helpers, hashing, random generation.
## Technical Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Node model | Single data class with 25+ properties | Aggregates all node-related data for convenient access |
| `isOnline` | Computed property vs `lastHeard` threshold | Simple, predictable, no caching needed |
| Colors | num-based deterministic generation | Consistent per-node colors without needing a color assignment system |
| Position validation | `latitude_i != 0 && longitude_i != 0` | Matches firmware convention; (0,0) treated as "no position" |
| Channel URL | Base64url without padding | Safe for URL embedding, matches Meshtastic web client |
| Version parsing | Regex-based `DeviceVersion` | Handles `major.minor.patch.hash` and `major.minor.patch` formats |
| Platform actuals | DateTime + Random only | Minimal surface; everything else is pure Kotlin |
## Gaps Identified
| Gap | Severity | Recommendation |
|-----|----------|----------------|
| `Node.distance()` has no unit test | ⚠️ Medium | Add test with known coordinate pairs |
| `Node.bearing()` has no unit test | ⚠️ Medium | Add test with cardinal direction verification |
| `Node.colors` has no test | ⚠️ Low | Verify contrast ratio for light/dark backgrounds |
| `DataPacket` has no test | ⚠️ Low | Test `nodeNumToDefaultId` conversion |
| `Message` has no test | ⚠️ Low | Test equality and display formatting |
| `Channel` has only androidDeviceTest | ⚠️ Low | Consider migrating to commonTest |
| `ChannelSet` has only androidDeviceTest | ⚠️ Medium | URL round-trip test should be in commonTest for cross-platform validation |
| `MeshDataMapper` has no test | ⚠️ Medium | Add tests for proto → domain mapping correctness |
| `TimeUtils` has no test | ⚠️ Low | Add formatting boundary tests |
| `DistanceExtensions` has no test | ⚠️ Low | Add metric ↔ imperial conversion tests |

View File

@@ -1,183 +0,0 @@
# Feature Specification: Core Model (Domain Models)
**Feature Branch**: `017-core-model`
**Created**: 2026-07-27
**Status**: Migrated
**Input**: Brownfield migration — reverse-engineered from existing `core/model` module
## Summary
Core Model defines the domain models, utility functions, and extensions used throughout Meshtastic-Android. It contains 57 `commonMain` files (plus platform actuals) providing the `Node` domain model (231 LOC), `DataPacket`, `Message`, `Contact`, `Channel`, `DeviceVersion`, `ConnectionState`, `SessionStatus`, and 25+ utility extensions for time, distance, coordinates, URL parsing, channel set encoding, and proto wire extensions. This module has no UI and no persistence — it is pure domain logic consumed by all other modules.
## Goals
1. **Unified domain vocabulary** — provide a single source of truth for mesh network domain types (Node, Channel, Contact, Message, etc.).
2. **Rich Node model** — aggregate user info, position, telemetry, hardware metadata, and computed properties (online status, distance, bearing, colors) in one data class.
3. **Utility library** — provide shared utility functions for time formatting, unit conversion, GPS formatting, URL construction/parsing, and proto extensions.
4. **Channel & URL handling** — encode/decode Meshtastic channel configuration URLs using protobuf + base64.
5. **Cross-platform** — all models and utilities in `commonMain` with minimal platform actuals.
## Non-Goals
- Persistence (no Room entities or DAOs) — handled by `core/database`.
- Business logic or data flow orchestration — handled by `core/data`.
- UI rendering or formatting beyond string generation — handled by feature modules.
- Proto message definitions — handled by `core/proto` (read-only upstream).
## User Scenarios & Testing *(mandatory)*
### User Story 1 — Node Domain Model (Priority: P1)
The `Node` data class is the primary domain model representing a mesh network participant. It aggregates user identity, position, device metrics, environment metrics, power metrics, and computed properties like `isOnline`, `distance`, `bearing`, `colors`, and `capabilities`.
**Why this priority**: `Node` is referenced by every feature module. Incorrect domain logic propagates everywhere.
**Independent Test**: Pure data class — fully testable without dependencies.
**Acceptance Scenarios**:
1. **Given** a node with `lastHeard` within the online threshold, **When** `isOnline` is evaluated, **Then** it returns `true`.
2. **Given** a node with `lastHeard` older than 15 minutes, **When** `isOnline` is evaluated, **Then** it returns `false`.
3. **Given** two nodes with valid positions, **When** `distance(other)` is called, **Then** it returns the distance in meters.
4. **Given** two nodes with valid positions, **When** `bearing(other)` is called, **Then** it returns the bearing in degrees.
5. **Given** a node num, **When** `colors` is accessed, **Then** it returns a foreground/background color pair derived from the num.
6. **Given** a node with `hw_model == UNSET`, **When** `isUnknownUser` is checked, **Then** it returns `true`.
7. **Given** `createFallback(nodeNum, prefix)`, **When** called, **Then** a Node with a generated user ID and long_name is returned.
---
### User Story 2 — Channel Set URL Encoding/Decoding (Priority: P1)
The `ChannelSet` utility encodes and decodes Meshtastic channel configuration URLs. These URLs allow users to share channel settings via QR codes and deep links.
**Why this priority**: Channel sharing is a core onboarding flow. Encoding bugs break channel provisioning.
**Independent Test**: Pure encoding/decoding — fully testable.
**Acceptance Scenarios**:
1. **Given** a `ChannelSet` protobuf, **When** encoded to URL, **Then** the result matches `https://meshtastic.org/e/#<base64>`.
2. **Given** a valid Meshtastic URL, **When** decoded, **Then** the `ChannelSet` protobuf is reconstructed.
3. **Given** a malformed URL, **When** decode is attempted, **Then** `MalformedMeshtasticUrlException` is thrown.
4. **Given** URL encoding uses base64url (no padding), **When** a URL is generated, **Then** it is safe for URL embedding.
---
### User Story 3 — Device Version & Capabilities (Priority: P2)
`DeviceVersion` parses firmware version strings. `Capabilities` derives feature availability from the firmware version (e.g., managed mode, remote admin, removal, etc.).
**Why this priority**: Feature gates depend on accurate version parsing. Wrong capabilities hide available features.
**Independent Test**: Pure string parsing — fully testable.
**Acceptance Scenarios**:
1. **Given** a firmware version string "2.5.19.abc1234", **When** parsed by `DeviceVersion`, **Then** major=2, minor=5, patch=19.
2. **Given** a firmware version ≥ minimum for managed mode, **When** `capabilities.hasManagedMode` is checked, **Then** it returns `true`.
3. **Given** an unknown firmware version string, **When** parsed, **Then** it falls back to a zero version without crashing.
---
### Edge Cases
- What happens when `Node.distance()` is called with invalid positions? Returns `null`.
- What happens when `ChannelSet` URL contains an unrecognized scheme? `MalformedMeshtasticUrlException` is thrown.
- What happens when `DataPacket.nodeNumToDefaultId()` receives 0? It generates a valid ID string.
- What happens when `CommonUtils` formats a negative node number? The hex formatting handles it correctly.
## Architecture
### Key Components
| Component | File | Purpose |
|-----------|------|---------|
| `Node` | `Node.kt` (231 LOC) | Primary domain model: user, position, metrics, computed properties |
| `DataPacket` | `DataPacket.kt` | Mesh data packet representation with node ID helpers |
| `Message` | `Message.kt` | Chat message domain model |
| `Contact` | `Contact.kt` | Contact/channel representation |
| `Channel` | `Channel.kt` | Channel configuration model |
| `ConnectionState` | `ConnectionState.kt` | App-level connection state enum |
| `DeviceVersion` | `DeviceVersion.kt` | Firmware version parser |
| `Capabilities` | `Capabilities.kt` | Feature capability flags derived from version |
| `SessionStatus` | `SessionStatus.kt` | Remote admin session status |
| `NodeSortOption` | `NodeSortOption.kt` | Node list sort options |
| `RadioController` | `RadioController.kt` | Radio control interface |
| `ChannelSet` | `util/ChannelSet.kt` | Channel URL encoding/decoding |
| `MeshDataMapper` | `util/MeshDataMapper.kt` | Proto → domain model mapping |
| `TimeUtils` | `util/TimeUtils.kt` | Time formatting utilities |
| `DateTimeUtils` | `util/DateTimeUtils.kt` | Date/time formatting |
| `DistanceExtensions` | `util/DistanceExtensions.kt` | Distance string formatting |
| `UnitConversions` | `util/UnitConversions.kt` | Metric ↔ imperial conversions |
| `LocationUtils` | `util/LocationUtils.kt` | Lat/long calculations |
| `GeoConstants` | `util/GeoConstants.kt` | Geographic constants |
| `Extensions` | `util/Extensions.kt` | General Kotlin extensions |
| `WireExtensions` | `util/WireExtensions.kt` | Proto Wire extensions |
| `ByteStringExtensions` | `util/ByteStringExtensions.kt` | Okio ByteString helpers |
| `ByteStringSerializer` | `util/ByteStringSerializer.kt` | kotlinx.serialization for ByteString |
| `CommonUtils` | `util/CommonUtils.kt` | Node ID formatting, hex utils |
| `SfppHasher` | `util/SfppHasher.kt` | Short-fast position-packet hasher |
| `NodeIdLookup` | `util/NodeIdLookup.kt` | Node num → display name resolution |
| `SharedContact` | `util/SharedContact.kt` | Shared contact for Android sharing |
| `UriUtils` | `util/UriUtils.kt` | URI construction helpers |
| `RandomUtils` | `util/RandomUtils.kt` | Random generation (expect/actual) |
| `TimeConstants` | `util/TimeConstants.kt` | Time-related constants |
| `MeshtasticUrlConstants` | `util/MeshtasticUrlConstants.kt` | URL scheme constants |
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: `Node` MUST compute `isOnline` based on `lastHeard` vs the online time threshold.
- **FR-002**: `Node` MUST compute `distance(other)` in meters between two nodes with valid positions.
- **FR-003**: `Node` MUST compute `bearing(other)` in degrees between two nodes with valid positions.
- **FR-004**: `Node` MUST derive foreground/background color pair from `num` using brightness-based contrast.
- **FR-005**: `Node.createFallback()` MUST generate a valid Node with generated user ID and display name.
- **FR-006**: `ChannelSet` MUST encode channel config to `https://meshtastic.org/e/#<base64url>` format.
- **FR-007**: `ChannelSet` MUST decode valid Meshtastic URLs back to `ChannelSet` protobuf.
- **FR-008**: `DeviceVersion` MUST parse firmware version strings in `major.minor.patch.hash` format.
- **FR-009**: `Capabilities` MUST derive feature availability flags from firmware version comparisons.
- **FR-010**: `SfppHasher` MUST produce consistent hashes for position packet deduplication.
### Non-Functional Requirements
- **NFR-001**: All models and utilities MUST reside in `commonMain` (Constitution §I).
- **NFR-002**: Platform actuals MUST be limited to `DateTimeActuals` and `RandomUtils` (JVM/Android/iOS).
- **NFR-003**: No business logic beyond domain model computations — no repository or service calls.
- **NFR-004**: `isUnmessageableRole()` MUST cover all non-messageable device roles.
## Source-Set Impact
| Source Set | Impact | Justification |
|-----------|--------|---------------|
| `commonMain` | 57 files (~4,500 LOC) | All models, utilities, extensions |
| `commonTest` | 6 files (~400 LOC) | ChannelOption, SfppHasher, CommonUtils, DeviceVersion, RouteDiscovery, Capabilities |
| `androidDeviceTest` | 3 files (~200 LOC) | SharedContact, ChannelSet, Channel tests |
| `jvmAndroidMain` | 2 files (~100 LOC) | DateTimeActuals, RandomUtils |
| `androidMain` | 2 files (~80 LOC) | UriBridge, PosixTimeZoneUtils |
| `iosMain` | 1 file (~20 LOC) | Noop stubs |
## Privacy Assessment
- [x] No PII in model definitions — node names are user-controlled, not PII
- [x] Position data is domain-level only — no logging or transmission from this module
- [x] Channel keys are not logged or serialized beyond URL encoding
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: `Node.isOnline` correctly evaluates against the time threshold for boundary values.
- **SC-002**: `Node.distance()` returns accurate distance (±1m) for known coordinate pairs.
- **SC-003**: `ChannelSet` URL round-trip: encode → decode produces identical protobuf.
- **SC-004**: `DeviceVersion` parses all known firmware version string formats.
- **SC-005**: `SfppHasher` produces consistent hashes across platforms (no platform-specific behavior).
- **SC-006**: All 9 existing test files pass with `allTests` target.
## Assumptions
- Proto messages are from `core/proto` — Wire-based protobuf classes.
- `onlineTimeThreshold()` is defined as a function (not a constant) to allow clock-relative computation.
- `MeshDataMapper` requires `NodeIdLookup` for node num → display name resolution.
- All display formatting methods return plain strings — Compose formatting is in UI modules.

View File

@@ -1,164 +0,0 @@
# Tasks: Core Model (Domain Models)
**Spec**: [spec.md](spec.md) | **Plan**: [plan.md](plan.md)
**Status**: Migrated — all existing tasks marked complete. Gap tasks marked incomplete.
**Task Prefix**: `MDL-T`
---
## Phase 1 — Domain Models
### MDL-T001: Node domain model [x]
- **File**: `Node.kt` (~231 LOC)
- 25+ properties: num, user, position, snr, rssi, lastHeard, deviceMetrics, environmentMetrics, powerMetrics, paxcounter, publicKey, notes, etc.
- Computed: `isOnline`, `colors`, `isUnknownUser`, `hasPKC`, `mismatchKey`, `validPosition`, `distance()`, `bearing()`, `gpsString()`, `getTelemetryStrings()`.
- Companion: `createFallback()`, `getRelayNode()`, `ERROR_BYTE_STRING`.
- Extension: `isUnmessageableRole()`.
- **Test**: Partial — verified via integration across all feature modules.
### MDL-T002: DataPacket [x]
- **File**: `DataPacket.kt`
- Mesh data packet representation: data, from, to, time, id, dataType, status.
- `nodeNumToDefaultId()`: converts node number to `!hex` format.
- **Test**: Verified via integration.
### MDL-T003: Message + Contact + Channel [x]
- **Files**: `Message.kt`, `Contact.kt`, `Channel.kt`
- `Message`: chat message with sender, timestamp, status, reactions.
- `Contact`: contact/channel representation with unread count, mute settings.
- `Channel`: channel config model with name, settings, role.
- **Test**: `ChannelTest.kt` (androidDeviceTest), verified via messaging feature.
### MDL-T004: ConnectionState + SessionStatus [x]
- **Files**: `ConnectionState.kt`, `SessionStatus.kt`
- `ConnectionState`: Disconnected, DeviceSleep, Connected (enum).
- `SessionStatus`: Active, Expired, NotEstablished (sealed).
- **Test**: Verified via service and remote admin features.
### MDL-T005: DeviceVersion + Capabilities [x]
- **Files**: `DeviceVersion.kt`, `Capabilities.kt`
- `DeviceVersion`: parses `major.minor.patch.hash` firmware strings.
- `Capabilities`: feature flags derived from version (managed mode, remote admin, etc.).
- **Test**: `DeviceVersionTest.kt`, `CapabilitiesTest.kt`.
### MDL-T006: Network models [x]
- **Files**: `NetworkFirmwareRelease.kt`, `NetworkDeviceHardware.kt`, `BootloaderOtaQuirk.kt`
- JSON-deserialized models for firmware release API and hardware catalog.
- **Test**: Verified via firmware update feature.
### MDL-T007: Supporting domain types [x]
- **Files**: `NodeSortOption.kt`, `NodeInfo.kt`, `MyNodeInfo.kt`, `InterfaceId.kt`, `DeviceType.kt`, `DeviceHardware.kt`, `MeshLog.kt`, `MqttConnectionState.kt`, `MqttProbeStatus.kt`, `MqttJsonPayload.kt`, `Reaction.kt`, `RouteDiscovery.kt`, `TracerouteOverlay.kt`, `NeighborInfo.kt`, `ChannelOption.kt`, `TAK.kt`, `MeshActivity.kt`, `EventEdition.kt`, `RadioController.kt`, `RadioNotConnectedException.kt`
- Various supporting domain types used across feature modules.
- **Test**: `ChannelOptionTest.kt`, `RouteDiscoveryTest.kt`.
### MDL-T008: Service action models [x]
- **Files**: `service/ServiceAction.kt`, `service/TracerouteResponse.kt`
- `ServiceAction`: sealed class for all service commands (send, config, traceroute, etc.).
- `TracerouteResponse`: traceroute result model.
- **Test**: Verified via service action routing.
---
## Phase 2 — Utilities & Extensions
### MDL-T009: ChannelSet URL encoding/decoding [x]
- **File**: `util/ChannelSet.kt`
- Encode: `ChannelSet` protobuf → `https://meshtastic.org/e/#<base64url>`.
- Decode: URL → `ChannelSet` protobuf.
- Error: `MalformedMeshtasticUrlException` for invalid URLs.
- **Test**: `ChannelSetTest.kt` (androidDeviceTest).
### MDL-T010: MeshDataMapper [x]
- **File**: `util/MeshDataMapper.kt`
- Maps proto messages (User, Position, DeviceMetrics, etc.) to domain models.
- Requires `NodeIdLookup` for node num → display name.
- **Test**: Verified via data layer integration.
### MDL-T011: Time and date utilities [x]
- **Files**: `util/TimeUtils.kt`, `util/DateTimeUtils.kt`, `util/TimeConstants.kt`
- Time formatting, relative time strings, epoch conversions, online threshold.
- Platform actuals in `jvmAndroidMain/DateTimeActuals.kt`.
- **Test**: Verified via UI integration.
### MDL-T012: Distance and location utilities [x]
- **Files**: `util/DistanceExtensions.kt`, `util/LocationUtils.kt`, `util/GeoConstants.kt`, `util/UnitConversions.kt`
- Haversine distance, bearing calculation, GPS formatting, metric/imperial conversion.
- **Test**: Verified via map and node detail features.
### MDL-T013: Common utilities and extensions [x]
- **Files**: `util/CommonUtils.kt`, `util/Extensions.kt`, `util/DebugUtils.kt`, `util/RandomUtils.kt`
- Node ID formatting, hex encoding, general Kotlin extensions, debug helpers.
- `RandomUtils`: expect/actual for platform-specific random generation.
- **Test**: `CommonUtilsTest.kt`.
### MDL-T014: Proto and byte extensions [x]
- **Files**: `util/WireExtensions.kt`, `util/ByteStringExtensions.kt`, `util/ByteStringSerializer.kt`
- Wire protobuf extensions, Okio ByteString helpers, kotlinx.serialization support.
- **Test**: Verified via data layer integration.
### MDL-T015: URL and URI utilities [x]
- **Files**: `util/UriUtils.kt`, `util/MeshtasticUrlConstants.kt`
- URL construction, Meshtastic URL scheme constants.
- **Test**: Verified via onboarding and channel sharing features.
### MDL-T016: SfppHasher [x]
- **File**: `util/SfppHasher.kt`
- Short-fast position-packet hasher for deduplication.
- **Test**: `SfppHasherTest.kt`.
### MDL-T017: SharedContact + NodeIdLookup [x]
- **Files**: `util/SharedContact.kt`, `util/NodeIdLookup.kt`
- Android sharing helper, node num → name lookup interface.
- **Test**: `SharedContactTest.kt` (androidDeviceTest).
---
## Gap Tasks (Incomplete)
### MDL-T018: Add Node domain model unit tests [x]
- **File to create**: `commonTest/.../NodeTest.kt`
- Test `isOnline` boundary values, `distance()` with known coordinates, `bearing()` cardinal directions, `colors` contrast, `createFallback()`, `getRelayNode()`.
- **Priority**: Medium
### MDL-T019: Add MeshDataMapper tests [x]
- **File to create**: `commonTest/.../util/MeshDataMapperTest.kt`
- Test proto → domain mapping for User, Position, DeviceMetrics, EnvironmentMetrics.
- **Priority**: Medium
### MDL-T020: Migrate ChannelSet tests to commonTest [ ]
- **File to create**: `commonTest/.../util/ChannelSetTest.kt`
- URL round-trip test should run on all platforms, not just Android.
- **Priority**: Medium
### MDL-T021: Add distance/location utility tests [ ]
- **File to create**: `commonTest/.../util/DistanceExtensionsTest.kt`
- Metric ↔ imperial conversion, distance formatting for known values.
- **Priority**: Low
### MDL-T022: Add DataPacket + Message tests [x]
- **File to create**: `commonTest/.../DataPacketTest.kt`
- Test `nodeNumToDefaultId`, equality, display formatting.
- **Priority**: Low