This document provides comprehensive architecture documentation and development guidance for the MeshMapper Flutter App.
MeshMapper Flutter App is a cross-platform wardriving application for MeshCore mesh network devices. It's a Flutter port of the MeshMapper WebClient, supporting Android, iOS, and Web (Chrome/Edge only).
Purpose: Connect to MeshCore devices via Bluetooth Low Energy, send GPS-tagged pings to the #wardriving channel, track repeater echoes, and post coverage data to the MeshMapper API for community mesh mapping.
Tech Stack: Flutter 3.2.0+, Dart 3.2.0+, Hive for local storage, Provider for state management
# Install dependencies
flutter pub get
# Run code generation (for Hive models)
flutter pub run build_runner build --delete-conflicting-outputs
# Run the app (API_KEY required — never hardcoded in source)
flutter run --dart-define=API_KEY=<your-key> # Android/iOS
flutter run -d chrome --dart-define=API_KEY=<your-key> # Web (Chrome required)
flutter run -d chrome --dart-define=API_KEY=<your-key> --web-browser-flag="--disable-web-security" # Web + CORS
# Analyze code
flutter analyze
# Run tests
flutter test
# Run a single test file
flutter test test/services/gps_service_test.dart# Use Build.sh — prompts for API key and signing passwords
./Build.sh
# Or set API key via environment variable to skip prompt
MESHMAPPER_API_KEY=<your-key> ./Build.sh- Web: Add
?debug=1to URL to enable debug logging in browser console - Mobile: Debug logging enabled in debug builds via
kDebugMode; disabled in release builds
The app uses a layered service architecture with clear separation of concerns:
Bluetooth Abstraction Layer (lib/services/bluetooth/):
BluetoothService: Abstract interface for BLE operationsMobileBluetoothService: Android/iOS implementation usingflutter_blue_plusWebBluetoothService: Web implementation usingflutter_web_bluetooth- Platform selection happens at runtime in
main.dartusingkIsWeb
MeshCore Protocol Layer (lib/services/meshcore/):
MeshCoreConnection: Implements the 9-step connection workflow and MeshCore companion protocolPacketParser: Binary packet parsing with BufferReader/Writer utilitiesUnifiedRxHandler: Routes ALL incoming BLE packets to TX tracking or RX loggingTxTracker: Detects repeater echoes during 7-second window after TX pingDiscTracker: Detects discovery responses during 7-second window after discovery requestRxLogger: Logs passive mesh observations, buffers by repeater IDChannelService: Channel hash computation and managementCryptoService: SHA-256 channel key derivation, AES-ECB message decryption
Application Services (lib/services/):
GpsService: GPS tracking with server-side zone validationPingService: TX/RX/Discovery ping orchestration, coordinates with TxTracker/DiscTracker/RxLoggerApiQueueService: Hive-based persistent upload queue with batch POST and retry logicApiService: HTTP client for MeshMapper API endpointsDeviceModelService: Loadsassets/device-models.jsonfor device identification and power reporting
State Management (lib/providers/):
AppStateProvider: Single ChangeNotifier for all app state using Provider pattern- All UI updates happen via
notifyListeners()after state mutations
Critical safety: The connection sequence MUST complete in order.
- BLE GATT Connect: Platform-specific BLE connection
- Protocol Handshake:
deviceQuery()with protocol version - Device Info:
deviceQuery()returns manufacturer string, thengetSelfInfo()acquires device public key (required for geo-auth API authentication). IfgetSelfInfo()fails, the entire connection fails. - Device Identification: Parse manufacturer string, match against
device-models.json(does NOT modify radio settings) - Time Sync:
sendTime()syncs device clock - Session Acquisition: POST to
/wardrive-api.php/authfor geo-auth session. Two-stage flow: first attempt with device public key, fallback to registration with signed contact URI if device not registered. Returnssession_id,tx_allowed,rx_allowed,expires_at, and regional channels. - Channel Setup: Create or use existing
#wardrivingchannel, plus any regional channels from auth response - GPS Init: Acquire GPS lock
- Connected State: Ready for wardriving — Unified RX Handler starts processing ALL incoming packets, noise floor polling begins (5s interval)
Important: The app does NOT modify the radio's TX power settings. It only identifies the device model to determine what power level to report in API calls. Users configure their radio's actual TX power through the device firmware.
Key Principle: Accept ALL incoming BLE packets, parse metadata ONCE at entry point, then route to specialized handlers. Never filter by header at entry.
Flow:
BLE LogRxData Event
↓
UnifiedRxHandler._handleLogRxData()
↓
Parse PacketMetadata (ONCE)
↓
┌────┴────┐
↓ ↓
TX Track RX Log
(echoes) (passive)
↓ ↓
7s window Buffer by repeater
↓ ↓
Update UI Flush to API queue
TX Tracking (during 7-second window after ping):
- Validates: GROUP_TEXT header, RSSI < -30dBm, channel hash match, decrypted message match, path length > 0
- Deduplicates by first hop (repeater ID), keeps best SNR
- Updates UI with repeater counts
RX Logging (continuous passive monitoring):
- Validates: path length > 0, valid GPS, channel hash in allowed list, decrypts successfully, 90% printable chars, RSSI < -30dBm
- Buffers per repeater with GPS coordinates
- Flushes to API queue on 25m movement OR 30s timeout
- Maintains in-memory log (max 100 entries) for UI
Discovery pings use the MeshCore control data protocol to directly query nearby repeaters and rooms, as opposed to TX pings which broadcast a channel message and listen for echoes.
BLE Command: sendControlData() (cmd 0x37) with DISCOVER_REQ flag (0x80), type filter for REPEATER|ROOM, and a random 4-byte tag.
Response: ControlData packets (0x8E) with DISCOVER_RESP flag (0x90), containing node type, remote SNR, and full 32-byte public key.
Tracking: DiscTracker manages a 7-second listening window (like TxTracker), validates responses, deduplicates by public key, and applies carpeater filtering (RSSI too strong = too close).
API Payload: Type "DISC" with fields: lat, lon, repeater_id, node_type, local_snr, local_rssi, remote_snr, public_key, timestamp, external_antenna, noisefloor.
Three auto-ping modes are available after connecting:
- Active Mode: Sends TX pings at user-configured interval (15s, 30s, or 60s). Each ping broadcasts a group channel message containing GPS location and radio power to
#wardriving, then listens 7s for repeater echoes viaTxTracker. - Passive Mode: Sends discovery requests every 30s. No TX pings — only discovery request-response. Responses tracked via
DiscTracker. - Hybrid Mode: Alternates between discovery and TX pings at the user-configured interval. Discovery → TX → Discovery → TX...
All modes also passively listen for RX packets via RxLogger, adding additional free coverage data to MeshMapper from nearby mesh traffic.
- Uses
geolocatorpackage with high accuracy and continuous tracking - Zone Validation: Server-side — client sends GPS coordinates to the API, server returns zone status (in-zone, nearest zone, or error)
- Min Distance Filter: 25m between pings prevents spam
Three data flows (TX pings, RX observations, Discovery results) merge into unified API batch queue:
- Storage: Hive-based persistent queue survives app restarts
- Batch Size: Max 50 messages, auto-flush at 10 items or 30 seconds
- Payload Format:
[{type:"TX"|"RX"|"DISC", ...}]— TX/RX includeheard_repeats; DISC includesrepeater_id,node_type,local_snr,local_rssi,remote_snr,public_key - Authentication: API key in JSON body (NOT query string)
- Retry Logic: Exponential backoff on failures
OfflineSessionService enables wardriving when the API is unavailable (no network, maintenance mode, etc.). Data accumulates locally and can be uploaded later.
- Storage: SharedPreferences with key
offline_sessions— JSON-encoded list of session objects - Session Format: Each session has a filename (
YYYY-MM-DD.json), creation timestamp, ping count, device info, and the wardrive data payload - Upload: Sessions can be uploaded via Settings screen when connectivity is restored
- Non-persistent: Offline mode is never persisted — always off on app restart. Users must re-enable if needed.
- Maintenance integration: When maintenance mode is detected while disconnected, the UI suggests using Offline Mode
- File:
lib/services/offline_session_service.dart
Keeps BLE and GPS active when the app is backgrounded during auto-ping.
- Android: Foreground service via
flutter_background_servicewith persistent low-importance notification (no sound/vibration). Notification shows live stats:TX: N | RX: M | Queue: P(Active/Hybrid) orRX: M | Queue: P(Passive). Foreground types:location + connectedDevice. - iOS: Uses declared background modes (
bluetooth-central,location). Users can enable "Background Location" in Settings to upgrade to "Always" location permission, which prevents iOS throttling during extended sessions. This must be manually enabled — a disclosure dialog explains the feature, then the system permission prompt appears. - Web: No-op (Web Bluetooth requires active tab)
- Lifecycle: Lazy-initialized on first
startService()call (triggered by auto-ping start), stopped on disconnect or auto-ping stop - Orphan cleanup:
cleanupOrphanedService()detects and stops stale foreground services from previous sessions - File:
lib/services/background_service.dart
Continuous RSSI measurement of the idle channel, providing ambient noise data for coverage analysis.
- Polling: 5-second interval via
MeshCoreConnection.getNoiseFloor()(MeshCore stats request for radio stats, parses int16LE). Retries up to 3 consecutive failures before stopping. - Sessions:
NoiseFloorSession(HiveType 13) records samples + ping event markers over time. Each sample has a timestamp and noise floor value (dBm). - Event Markers:
PingEventMarkerrecords ping events overlaid on the noise floor graph:txSuccess(Green) — TX heard by repeatertxFail(Red) — TX not heardrx(Blue) — Passive RX receiveddiscSuccess(Purple) — Discovery got responsediscFail(Grey) — Discovery no response- Each marker includes repeater info (ID, SNR, RSSI, optional public key for discovery)
- Visualization: Interactive chart (
NoiseFloorChartwidget) with:- Color-coded noise floor line: green (-120 to -100 dBm), orange (-100 to -90 dBm), red (-90+ dBm)
- Pinch-to-zoom with focal point tracking, pan support, 10s minimum visible window
- Tap markers to show detail sheet with event type, timestamp, interpolated noise floor, and repeater table
- API Integration:
noisefloorfield included in every TX/RX/DISC API payload - Files:
lib/models/noise_floor_session.dart,lib/widgets/noise_floor_chart.dart
"Carpeater" = co-located repeater with very strong signal, indicating the device is too close for meaningful coverage data.
- RSSI threshold: Packets with RSSI >= -30 dBm are automatically dropped as carpeater (constant
maxRssiThreshold) - User filter: Optional repeater ID blocklist configured by the user — checked via
shouldIgnoreRepeater()before RSSI validation - Applied in:
PacketValidator(shared validation used by TxTracker, DiscTracker, and RxLogger) - Validation pipeline: RSSI check → packet type (GROUP_TEXT/ADVERT) → channel hash match → AES-ECB decryption → printable character ratio (60% minimum)
- Logging: Carpeater drops logged to error log without auto-switching tabs, using
[RX FILTER]debug tag - File:
lib/services/meshcore/packet_validator.dart
Two-service system for capturing debug logs and submitting bug reports.
DebugFileLogger:
- Writes timestamped log files (
meshmapper-debug-{unix_timestamp}.txt) to app documents directory - Auto-rotation: max 10 files, max 4.5 MB per chunk (0.5 MB safety margin under 5 MB server limit)
- 5-second flush timer (critical for iOS background suspension)
- Non-persistent: always starts disabled on app launch
- Log format:
[ISO8601_timestamp] LEVEL: message
DebugSubmitService — 4-step bug report workflow:
- Create Ticket (0-20%): POST to
/debug/submitdebug.php/create-ticket→ returnsissue_number,issue_url - Request Upload (per file, 20-90%): POST
/request-upload→ returnsupload_url,session_id - Upload File: POST multipart to
upload_url— splits large files at newline boundaries, uploads chunks sequentially with retry (3 attempts, exponential backoff) - Complete Upload (90-100%): POST
/upload-completewith issue reference
- Accessible via: Settings screen
- Files:
lib/services/debug_file_logger.dart,lib/services/debug_submit_service.dart
Sound notifications for TX pings and RX observations, configurable on/off.
- Sounds:
assets/transmitted_packet.mp3(TX/Discovery sent),assets/received_packet.mp3(repeater echo/RX received) - Storage: Hive box
audio_preferenceswith keysound_enabled - Audio focus: Android uses transient focus with ducking (Android Auto compatible). iOS uses ambient category (plays alongside other audio).
- Resilience: 3-second timeout protection prevents indefinite hangs from audio session corruption. On timeout, resets session and reloads assets.
- File:
lib/services/audio_service.dart
Prevents session timeout during long wardriving sessions by periodically refreshing the session expiry.
- Trigger: Enabled when auto-ping mode starts (
enableHeartbeat()), disabled on disconnect or leaving auto mode - Timing: Heartbeat fires 1 minute before session
expires_at. If already expired, sends immediately. - Mechanism: POST to
/wardrive-api.php/wardrivewithheartbeat: trueflag and optional GPS coordinates - Response: Returns updated
expires_at, which schedules the next heartbeat - Flow: Auth response sets initial
expires_at→ each wardrive POST or heartbeat updates it → timer reschedules automatically
Two-flag system ensuring users explicitly declare their antenna configuration before wardriving.
externalAntenna(bool): Whether an external antenna is connectedexternalAntennaSet(bool): Whether the user has explicitly configured this preference- Enforcement: UI requires user to set this before first ping (
PingValidation.externalAntennaRequired). Cannot be skipped. - API integration:
external_antennafield included in every TX/RX/DISC API payload - Persistence: Stored per-device, restored on reconnect with same device, reset on reconnect failure
Keeps the screen on during auto-ping to prevent device sleep during wardriving sessions.
- Enable: Called when auto-ping starts
- Disable: Called when auto-ping stops or on disconnect
- Package:
wakelock_plus - Platform: Android and iOS only (Web N/A — always requires active tab)
- File:
lib/services/wakelock_service.dart
- Service:
6E400001-B5A3-F393-E0A9-E50E24DCCA9E - RX Characteristic:
6E400002-B5A3-F393-E0A9-E50E24DCCA9E(write to device) - TX Characteristic:
6E400003-B5A3-F393-E0A9-E50E24DCCA9E(notifications from device)
- Default channels:
Public(fixed key8b3387e9c5cdea6ac9e5edbaa115cd72) and#wardriving(SHA-256 hash of channel name) - Regional channels: Additional channels (e.g.,
#ottawa,#testing) delivered by the API after auth, based on the user's zone - Channel hash (PSK identifier) used for repeater echo detection and message decryption (AES-ECB via pointycastle)
- Custom binary protocol with header byte (0x11 = GROUP_TEXT, 0x21 = ADVERT)
- Path encoding:
pathLenbyte encodes hash size (top 2 bits) + hop count (bottom 6 bits), followed byhopCount * hashSizepath bytespathHashSize = (pathLen >> 6) + 1→ 1, 2, 3, or 4 bytes per hoppathHashCount = pathLen & 63→ 0-63 hops
- SNR/RSSI metadata in BLE event payload
- Encrypted message payload (AES-ECB with channel key)
- Purpose: Expands repeater ID space from 256 (1-byte) to 65K (2-byte) or 16M (3-byte) unique IDs
- TX mode: Configured via
CMD_SET_PATH_HASH_MODE = 61 (0x3D)—[0x3D][0x00][mode]where mode=0→1-byte, 1→2-byte, 2→3-byte - RX auto-detect: Each received packet's
pathLenbyte is decoded to determine hash size, regardless of the user's TX setting - DeviceInfo: v10+ firmware includes
path_hash_modebyte after manufacturer + firmware version fields - API enforcement: Auth response may include
hop_bytes(1/2/3) to enforce regional path byte size - Lifecycle: Radio mode is set during connection and restored to original on clean disconnect. Unclean disconnect leaves radio in configured mode.
- Discovery pings: NOT affected — multi-byte paths apply only to TX/RX channel messages
- Safari NOT supported (no Web Bluetooth API)
- Uses
flutter_web_bluetoothpackage - Debug logging enabled via URL parameter
?debug=1 - CORS issues during local development - use
--web-browser-flag="--disable-web-security"
- Requires permissions: Bluetooth, Location (for BLE scanning)
- minSdkVersion: 21 (Android 5.0+)
- Background location permission for continuous tracking
- Uses
flutter_blue_pluspackage
- Requires Info.plist entries: NSBluetoothAlwaysUsageDescription, NSLocationWhenInUseUsageDescription
- Deployment target: 12.0+
- Background modes: bluetooth-central, location
- Uses
flutter_blue_pluspackage
Key packages used in this project:
flutter_blue_plus: Mobile Bluetooth (Android/iOS)flutter_web_bluetooth: Web Bluetooth (Chrome/Edge)geolocator: GPS/Locationmaplibre_gl: Map rendering (MapLibre GL vector tiles via OpenFreeMap)hive: Local storageprovider: State managementhttp: API requestspointycastle: Encryption (AES-ECB, SHA-256)
All debug log messages MUST include a tag in square brackets. Use the debug helper functions from utils/debug_logger_io.dart:
debugLog(message)— General debug informationdebugWarn(message)— Warning conditionsdebugError(message)— Error conditions
debugLog('[BLE] Connection established');
debugLog('[GPS] Fresh position acquired: lat=45.12345');
debugWarn('[PING] GPS data is stale, requesting fresh position');
debugError('[API] Failed to post batch: $error');Required Tags:
| Tag | Description |
|---|---|
[BLE] |
Bluetooth connection and device communication |
[CONN] |
MeshCore connection protocol operations |
[GPS] |
GPS/geolocation operations |
[PING] |
Ping sending and validation |
[API QUEUE] |
API queue operations (batch posting) |
[RX BATCH] |
RX batch buffer operations |
[RX] |
RX packet handling and logging |
[TX] |
TX packet handling and logging |
[DECRYPT] |
Message decryption |
[CRYPTO] |
Cryptographic operations (SHA-256, AES) |
[UI] |
General UI updates (status bar, buttons, etc.) |
[CHANNEL] |
Channel setup and management |
[TIMER] |
Timer and countdown operations |
[WAKE LOCK] |
Wake lock acquisition/release (legacy, prefer [WAKELOCK]) |
[GEOFENCE] |
Geofence and distance validation |
[CAPACITY] |
Capacity check API calls |
[AUTO] |
Auto mode operations (TX/RX or RX-only) |
[INIT] |
Initialization and setup |
[AUTH] |
Authentication API operations |
[HEARTBEAT] |
Session heartbeat operations |
[API] |
General API operations |
[MODEL] |
Device model identification and power reporting |
[MAP] |
Map widget operations |
[DISC] |
Discovery ping operations |
[MAINTENANCE] |
Maintenance mode handling |
[RX FILTER] |
RX packet validation and carpeater filtering |
[AUDIO] |
Audio/sound notification operations |
[BACKGROUND] |
Background mode and foreground service |
[DEBUG] |
Debug file logging and submission |
[GRAPH] |
Noise floor graph operations |
[HYBRID] |
Hybrid mode ping alternation |
[OFFLINE] |
Offline mode operations |
[SCAN] |
BLE device scanning |
[WAKELOCK] |
Wake lock acquisition/release |
Never log without a tag.
Use the status update methods in AppStateProvider for all UI status updates. Available status types:
idle— Default/waiting statesuccess— Successful operationswarning— Warning conditionserror— Error statesinfo— Informational/in-progress states
When modifying code, update DEVELOPMENT.md (this file) for architectural changes.
- Added debug logging with tags to new code
- Updated
DEVELOPMENT.mdif architecture changed - Added inline comments for complex logic
- Added Dart doc comments (
///) for public APIs
- Use Dart documentation comments (
///) for public classes and methods - Prefer
async/awaitover.then()chains - Always wrap async operations in
try/catchblocks - Use
debugError()for logging errors before handling - State mutations via
AppStateProviderwithnotifyListeners()
File: assets/device-models.json
Contains 30+ MeshCore device variants with manufacturer strings, TX power levels, and platform info:
- Ikoka: Stick, Nano, Handheld (22dBm, 30dBm, 33dBm variants)
- Heltec: V2, V3, V4, Wireless Tracker, MeshPocket
- RAK: 4631, 3x72
- LilyGo: T-Echo, T-Deck, T-Beam, T-LoRa
- Seeed: Wio E5, T1000, Xiao variants
Detection Flow:
deviceQuery()returns manufacturer string (e.g., "Ikoka Stick-E22-30dBm (Xiao_nrf52)nightly-e31c46f")parseDeviceModel()strips build suffix ("nightly-COMMIT")findDeviceConfig()searches database for exact/partial matchautoSetPowerLevel()configures radio power automatically
Critical Safety: PA amplifier models MUST use specific power values:
- 33dBm models: txPower=9, power=2.0
- 30dBm models: txPower=20, power=1.0
- Standard (22dBm): txPower=22, power=0.3
Base URL: https://meshmapper.net/
API Key: Injected at build time via --dart-define=API_KEY=.... Never hardcoded in source. Build.sh prompts for it, or set MESHMAPPER_API_KEY env var.
- POST /wardrive-api.php/status: Check zone status (geo-auth)
- POST /wardrive-api.php/auth: Acquire/release session (geo-auth)
- POST /wardrive-api.php/wardrive: Submit wardrive data + heartbeat
- Auth: API key in JSON body (
keyfield), NOT query string
All API endpoints may return maintenance mode:
{
"maintenance": true,
"maintenance_message": "Scheduled maintenance until 3:00 PM EST",
"maintenance_url": "https://meshmapper.net/status"
}- Disconnected: Blocks connecting, shows maintenance message on Connection screen with suggestion to use Offline Mode
- Connected: Ends session, logs to error log, navigates to error log tab
- Offline Mode: Users can still wardrive in Offline Mode during maintenance and upload data later when service is restored
-
Unified RX Handler accepts ALL packets - No header filtering at entry point. Session log tracking filters headers internally.
-
GPS freshness - The client doesn't enforce GPS freshness for pings (25m movement check is sufficient), but zone status checks require GPS < 60s old and < 50m accuracy. The server also enforces fresh GPS on submitted wardrive data.
-
Control locking during ping lifecycle -
sendPing()disables all controls until API post completes. Must call unlock in ALL code paths (success/error). -
Disconnect cleanup has 3 different flows:
- User disconnect: Full cleanup — stop auto-ping → end noise floor session → stop background service → flush RX logger → clear API queue → release session (
/authwithreason: disconnect) → delete wardriving channel (while BLE still connected) → close BLE → dispose all services → reset state - Unexpected BLE disconnect: Partial cleanup — preserves API session, API queue, and noise floor session for reconnection. Stops timers and background service, disposes BLE-dependent objects, then starts auto-reconnect with exponential backoff (max 30s timeout). On reconnect success, restores auto-ping if it was active.
- Reconnect failure / abandoned: Falls back to full disconnect cleanup — flushes and clears API queue, releases session, resets antenna preference (user must re-select)
Critical: Channel deletion MUST happen while BLE is still connected to avoid GATT errors. API queue is cleared on user disconnect (pings won't have valid session) but preserved during auto-reconnect.
- User disconnect: Full cleanup — stop auto-ping → end noise floor session → stop background service → flush RX logger → clear API queue → release session (
-
Platform-specific Bluetooth imports - Use conditional exports (bluetooth_service.dart exports platform-specific implementation). Never import platform-specific files directly.
-
Hive model generation required - After modifying
@HiveTypeclasses, runflutter pub run build_runner build --delete-conflicting-outputs. -
Web Bluetooth requires HTTPS - Development uses
flutter run -d chromewhich works, but production deployment needs HTTPS.
lib/main.dart- App entry point, platform detection, themelib/providers/app_state_provider.dart- Global state managementlib/services/meshcore/connection.dart- 9-step connection workflow, MeshCore protocollib/services/meshcore/unified_rx_handler.dart- Packet routing (TX vs RX)lib/services/meshcore/tx_tracker.dart- Repeater echo detection (7s window)lib/services/meshcore/disc_tracker.dart- Discovery response tracking (7s window)lib/services/meshcore/rx_logger.dart- Passive observation logginglib/services/ping_service.dart- TX/RX/Discovery ping orchestrationlib/services/gps_service.dart- GPS tracking and geofencinglib/services/api_queue_service.dart- Persistent upload queuelib/services/device_model_service.dart- Device model identificationlib/services/background_service.dart- Background operation (Android foreground service, iOS background modes)lib/services/audio_service.dart- Sound notifications for TX/RX eventslib/services/offline_session_service.dart- Offline wardriving session storagelib/services/debug_file_logger.dart- Debug log file rotation and uploadlib/services/debug_submit_service.dart- Bug report submission (4-step workflow)lib/services/gps_simulator_service.dart- GPS simulation for testinglib/services/wakelock_service.dart- Screen wake lock during auto-pinglib/services/meshcore/packet_validator.dart- Packet validation and carpeater filteringlib/models/noise_floor_session.dart- Noise floor session data modelslib/widgets/noise_floor_chart.dart- Noise floor graph visualizationassets/device-models.json- Device database (30+ models)