diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 05c82de..8826c55 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,8 +2,6 @@ name: Build on: push: - branches: - - main pull_request: jobs: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index c3926c9..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Deploy to Cloudflare Workers - -on: - push: - tags: - - '*' - workflow_dispatch: - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - # Match local development version which provides Dart 3.11.0 - flutter-version: '3.41.2' - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Get dependencies - run: flutter pub get - - - name: Build Web - run: bun run build - - - name: Deploy to Cloudflare - uses: cloudflare/wrangler-action@v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: deploy diff --git a/.github/workflows/flutter_dart.yml b/.github/workflows/flutter_analyze.yml similarity index 61% rename from .github/workflows/flutter_dart.yml rename to .github/workflows/flutter_analyze.yml index 117eb4f..af4a3b7 100644 --- a/.github/workflows/flutter_dart.yml +++ b/.github/workflows/flutter_analyze.yml @@ -1,10 +1,8 @@ -name: Flutter and Dart +name: Flutter Analyze on: pull_request: push: - branches: - - main jobs: analyze: @@ -21,11 +19,5 @@ jobs: - name: Install dependencies run: flutter pub get - - name: Analyze code + - name: Analyze run: flutter analyze --fatal-infos --fatal-warnings - - - name: Verify formatting - run: dart format --output=none --set-exit-if-changed . - - - name: Run tests - run: flutter test -r github diff --git a/.gitignore b/.gitignore index 88295e7..b918113 100644 --- a/.gitignore +++ b/.gitignore @@ -30,12 +30,8 @@ migrate_working_dir/ .flutter-plugins-dependencies .pub-cache/ .pub/ -pubspec.lock /build/ /coverage/ -# fvm project files -.fvm/ -.fvmrc # Symbolication related app.*.symbols @@ -61,7 +57,6 @@ secrets.dart .DS_Store .AppleDouble .LSOverride -macos/Flutter/GeneratedPluginRegistrant.swift # iOS **/ios/Pods/ @@ -70,7 +65,6 @@ macos/Flutter/GeneratedPluginRegistrant.swift **/ios/Flutter/Flutter.podspec # Android -.gradle/ **/android/.gradle/ **/android/captures/ **/android/local.properties @@ -87,6 +81,3 @@ keystore.properties # IDE .vscode/launch.json .vscode/settings.json - -# Cloudflare Wrangler -.wrangler diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29..0000000 diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index fcdb2e1..0000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -4.0.0 diff --git a/.swift-version b/.swift-version deleted file mode 100644 index 31b44b0..0000000 --- a/.swift-version +++ /dev/null @@ -1 +0,0 @@ -6.2.4 \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 273bb96..bac981d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ ## BLE Frames & Protocol Notes - Nordic UART Service (NUS) UUIDs: Service `6e400001-b5a3-f393-e0a9-e50e24dcca9e`, RX `6e400002-b5a3-f393-e0a9-e50e24dcca9e`, TX `6e400003-b5a3-f393-e0a9-e50e24dcca9e`. -- Discovery: scans for device names matching known prefixes and filters by `platformName`/`advertisementData.advName`. +- Discovery: scans for device name prefix `MeshCore-` and filters by `platformName`/`advertisementData.advName`. - Frames are capped at `maxFrameSize = 172` bytes; byte 0 is the command/response/push code. I/O is `MeshCoreConnector.sendFrame` and `MeshCoreConnector.receivedFrames`. - Command codes (to device): `cmdAppStart`=1, `cmdSendTxtMsg`=2, `cmdSendChannelTxtMsg`=3, `cmdGetContacts`=4, `cmdGetDeviceTime`=5, `cmdSetDeviceTime`=6, `cmdSendSelfAdvert`=7, `cmdSetAdvertName`=8, `cmdAddUpdateContact`=9, `cmdSyncNextMessage`=10, `cmdSetRadioParams`=11, `cmdSetRadioTxPower`=12, `cmdResetPath`=13, `cmdSetAdvertLatLon`=14, `cmdRemoveContact`=15, `cmdShareContact`=16, `cmdExportContact`=17, `cmdImportContact`=18, `cmdReboot`=19, `cmdSendLogin`=26, `cmdGetChannel`=31, `cmdSetChannel`=32, `cmdGetRadioSettings`=57. - Response codes (from device): `respCodeOk`=0, `respCodeErr`=1, `respCodeContactsStart`=2, `respCodeContact`=3, `respCodeEndOfContacts`=4, `respCodeSelfInfo`=5, `respCodeSent`=6, `respCodeContactMsgRecv`=7, `respCodeChannelMsgRecv`=8, `respCodeCurrTime`=9, `respCodeNoMoreMessages`=10, `respCodeContactMsgRecvV3`=16, `respCodeChannelMsgRecvV3`=17, `respCodeChannelInfo`=18, `respCodeRadioSettings`=25. diff --git a/CLAUDE.md b/CLAUDE.md index 55af890..08ef342 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,7 +61,7 @@ lib/ - **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device) ### Device Discovery -- Scans for devices with known name prefixes +- Scans for devices with name prefix `MeshCore-` - Filters by `platformName` or `advertisementData.advName` ### Connection States diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index ac727ba..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,71 +0,0 @@ -# How to contribute to Meshcore Open - -Before submitting any pull requests (PR), please review the following information. - -Unsolicited PRs without previous discussion or open issues may be -rejected. As may changes that are too broad (i.e. 100 files changed) or that -cover too many separate changes. If the changes are clearly AI generated they -may also be rejected. [See more](#ai-use) - -## First Step Checklist - -### **Did you find a bug?** - -* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/zjs81/meshcore-open/issues). - -* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/zjs81/meshcore-open/issues/new). -Be sure to include a **title and clear description**, as much relevant -information as possible, and a **code sample** or an **executable test case** -demonstrating the expected behavior that is not occurring. You can also include -screenshots or video. - -* DO NOT start work and submit a PR at this time, please discuss the issue and -your implementation plan first. - -### **Did you fix whitespace, format code, or make a purely cosmetic patch?** - -Changes that are cosmetic in nature and do not add anything substantial to the -stability, functionality, or testability of the application will generally not -be accepted. - -### **Do you intend to add a new feature or change an existing one?** - -* Suggest your change in a new issue as a feature request. - -* DO NOT start work and submit a PR at this time, please discuss the change and -your implementation plan first. - -* After it is generally decided that the feature or change fits the goals of the -project you can start work or open a PR if you have already started. - -## Submitting your patch - -* All changes should be based on the `dev` branch. When creating your PR please -be sure to change the target to merge into dev, and when starting work on a new -branch be sure to start on latest `dev`. - -* Ensure the PR description clearly describes the problem and solution. Include -the relevant issue number if applicable. - -* The PR should contain **one commit** only, the commit message should have a -clear title followed by a new line and then brief description if needed. PR with -multiple commits will be squashed into one before merging if required. See -[Git Mastery](https://git-mastery.org/lessons/commitMessage/) for more -information on good commit messages. - -* **Before committing changes** on your branch, be sure to run both -`dart format .` and `flutter analyze`. The continuous development checks will -fail if issues here are not addressed before hand. - -## AI-use - -Everyone loves some help, AI agents are a tool in many of our belts. The project -is not anti-AI. - -There are some limits to acceptable use however. Generally: - -* All code generated by AI should be thoroughly reviewed by the contributor. -* The changes should be tightly controlled to not change anything out of scope -for the patch, bug fix, etc. -* The contributor should have a good understanding of what the code does and how -the application works in order to effectively be able to manage the agent. diff --git a/README.md b/README.md index ac188f6..2acb390 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,6 @@ Open-source Flutter client for MeshCore LoRa mesh networking devices. MeshCore Open is a cross-platform mobile application for communicating with MeshCore LoRa mesh network devices via Bluetooth Low Energy (BLE). The app enables long-range, off-grid communication through peer-to-peer messaging, public channels, and mesh networking capabilities. - - Get it on Obtainium - - ## Screenshots @@ -25,7 +21,6 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ## Features ### Core Functionality - - **Direct Messaging**: Private encrypted conversations with individual contacts - **Public Channels**: Broadcast messages to channel subscribers on the mesh network - **Contact Management**: Organize contacts, track last seen times, and manage conversation history @@ -34,7 +29,6 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - **Message Replies**: Thread conversations with inline reply functionality ### Mesh Network - - **Path Visualization**: View routing paths and signal quality for each contact - **Route Management**: Manual path overriding and automatic route rotation - **Signal Metrics**: Real-time SNR (Signal-to-Noise Ratio) tracking @@ -42,7 +36,6 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - **Repeater Support**: Connect to and manage repeater nodes for extended range ### Map & Location - - **Live Map View**: Real-time visualization of mesh network nodes on an interactive map - **Node Filtering**: Filter by node type (chat, repeater, sensor) and time range - **Location Sharing**: Share GPS coordinates and custom markers with contacts @@ -50,14 +43,12 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - **MGRS Coordinates**: Support for Military Grid Reference System coordinate format ### Device Management - -- **BLE, USB, TCP Connection**: Scan and connect to MeshCore devices via Bluetooth, USB or TCP +- **BLE Connection**: Scan and connect to MeshCore devices via Bluetooth - **Device Settings**: Configure radio parameters, power settings, and network options - **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves - **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon) ### Repeater Hub - - **CLI Access**: Full command-line interface to repeater nodes - **Settings Management**: Configure repeater behavior, power limits, and network settings - **Statistics Dashboard**: View repeater traffic, connected clients, and system health @@ -66,7 +57,6 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ## Technical Details ### Architecture - - **Framework**: Flutter 3.38.5 / Dart 3.10.4 - **State Management**: Provider pattern with ChangeNotifier - **BLE Protocol**: Nordic UART Service (NUS) over Bluetooth Low Energy @@ -74,20 +64,11 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - **Encryption**: End-to-end encryption for private messages using the MeshCore protocol ### Platform Support - -| Feature | Android (API 21+) | iOS (12+) | Linux | Windows | macOS | Web | -|--------------------|:-----------------:|:---------:|:-----:|:-------:|:-----:|:---------------------------------:| -| BLE companion | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| USB companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ✅ | -| TCP companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ❌
(requires websocket bridge) | -| Core Functionality | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Mesh Network | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Map & Location | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Device Management | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Repeater Hub | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +- ✅ **Android**: Full support (API 21+) +- ✅ **iOS**: Full support (iOS 12+) +- 🚧 **Desktop**: Limited support (macOS/Linux/Windows) ### Dependencies - | Package | Purpose | |---------|---------| | flutter_blue_plus | Bluetooth Low Energy communication | @@ -103,7 +84,6 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ## Getting Started ### Prerequisites - - Flutter SDK 3.38.5 or later - Android Studio / Xcode (for mobile development) - A MeshCore-compatible LoRa device @@ -111,20 +91,17 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ### Installation 1. **Clone the repository** - ```bash git clone https://github.com/zjs81/meshcore-open.git cd meshcore-open ``` 2. **Install dependencies** - ```bash flutter pub get ``` 3. **Run the app** - ```bash flutter run ``` @@ -132,13 +109,11 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ### Building for Release **Android APK:** - ```bash flutter build apk --release ``` **iOS:** - ```bash flutter build ios --release ``` @@ -150,8 +125,7 @@ lib/ ├── main.dart # App entry point ├── connector/ │ ├── meshcore_connector.dart # BLE communication & state management -│ ├── meshcore_protocol.dart # Protocol definitions & frame parsing -│ └── meshcore_uuids.dart # Device names and IDs (add prefixes here!) +│ └── meshcore_protocol.dart # Protocol definitions & frame parsing ├── screens/ │ ├── scanner_screen.dart # Device scanning (home screen) │ ├── contacts_screen.dart # Contact list @@ -178,39 +152,25 @@ lib/ ## BLE Protocol ### Nordic UART Service (NUS) - - **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e` - **RX Characteristic**: `6e400002-b5a3-f393-e0a9-e50e24dcca9e` (Write to device) - **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device) ### Device Discovery - -Devices are discovered by scanning for BLE advertisements with known MeshCore device name prefixes. These are currently: - - `MeshCore-` - - `Whisper-` - - `WisCore-` - - `HT-` - - `LowMesh_MC_` - -New device prefixes can be added in `lib/connector/meshcore_uuids.dart`. - +Devices are discovered by scanning for BLE advertisements with the name prefix `MeshCore-` ### Message Format - Messages are transmitted as binary frames using a custom protocol optimized for LoRa transmission. See `meshcore_protocol.dart` for frame structure definitions. ## Configuration ### App Settings - - **Theme**: System default, light, or dark mode -- **Language**: Use one of 15 languages (English, Chinese, French, Spanish, Portuguese, German, Dutch, Polish, Swedish, Italian, Slovak, Slovene, Bulgarian, Russian, Ukrainian) - **Notifications**: Configurable for messages, channels, and node advertisements - **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types - **Message Retry**: Automatic retry with configurable path clearing ### Device Settings - - **Radio Power**: Transmit power adjustment (10-30 dBm) - **Frequency**: LoRa frequency configuration - **Bandwidth**: Channel bandwidth selection @@ -222,24 +182,22 @@ Messages are transmitted as binary frames using a custom protocol optimized for This is an open-source project. Contributions are welcome! ### Development Guidelines - - Follow the Flutter style guide - Use Material 3 design components - Write clear commit messages - Test on both Android and iOS before submitting PRs ### Code Style - - Prefer `StatelessWidget` with `Consumer` for reactive UI - Use `const` constructors where possible - Keep functions small and focused - Avoid premature abstractions -- Run dart format on all changes before submitting + ## Support For issues, questions, or feature requests, please open an issue on GitHub: - +https://github.com/zjs81/meshcore-open/issues ## Donate @@ -247,11 +205,6 @@ If you find MeshCore Open useful and would like to support development, you can **Solana Address:** `F15YanjZj96YTBtKJYgNa8RLQLCZkx5CEwogPWkqXeoQ` - -**Monero Address:** `453TxnpUqjkJtXxzdjMsrgERNkBRXEGamPbpC45ENrvKAk9tH7kZbxWF82Hz66etgDZyXFPEBU2JUEqhLeJyWt9kBvTVy5m` - -**Bitcoin Address:** `bc1qh45x28v8dslcg4v4upmqd9g0mvc3lnyffmyzr5` - Your support helps maintain and improve this open-source project! ## Acknowledgments diff --git a/TESTFLIGHT_GUIDE.md b/TESTFLIGHT_GUIDE.md deleted file mode 100644 index b092678..0000000 --- a/TESTFLIGHT_GUIDE.md +++ /dev/null @@ -1,244 +0,0 @@ -# TestFlight and App Store Deployment Guide - -## Prerequisites - -- [x] Apple Developer Account ($99/year) - [developer.apple.com](https://developer.apple.com) -- [x] Xcode installed -- [x] Apple Transporter app installed -- [x] App icons ready (1024x1024px) -- [x] Bundle ID configured: `com.monitormx.meshcoreopen` - -## Step 1: Register Bundle Identifier - -1. Go to [Apple Developer - Identifiers](https://developer.apple.com/account/resources/identifiers/list) -2. Click the **"+"** button -3. Select **"App IDs"** → Continue -4. Select **"App"** → Continue -5. Fill in: - - **Description**: Meshcore Open - - **Bundle ID**: Explicit - `com.monitormx.meshcoreopen` - - **Capabilities**: Leave defaults (or add as needed) -6. Click **Continue** → **Register** - -## Step 2: Create App in App Store Connect - -1. Go to [App Store Connect](https://appstoreconnect.apple.com) -2. Sign in with your Apple ID -3. Click **"My Apps"** -4. Click the **"+"** button → **"New App"** -5. Fill in the form: - - **Platforms**: iOS - - **Name**: Meshcore Open - - **Primary Language**: English (U.S.) - - **Bundle ID**: Select `com.monitormx.meshcoreopen` from dropdown - - **SKU**: `meshcore-open-001` (or any unique identifier) - - **User Access**: Full Access -6. Click **"Create"** - -## Step 3: Build the IPA - -Run these commands from the project directory: - -```bash -# Add CocoaPods to PATH -export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH" - -# Clean previous builds -../flutter/bin/flutter clean - -# Build IPA for App Store -../flutter/bin/flutter build ipa -``` - -The IPA will be created at: `build/ios/ipa/meshcore_open.ipa` - -## Step 4: Upload to App Store Connect via Transporter - -1. **Open Apple Transporter** - - Launch from Applications folder - - Sign in with your Apple ID - -2. **Upload the IPA** - - Drag and drop `build/ios/ipa/meshcore_open.ipa` into Transporter - - Click **"Deliver"** - - Wait for upload to complete (usually 1-5 minutes) - -3. **Processing** - - Apple will process your build (10-30 minutes) - - You'll receive an email when processing is complete - -## Step 5: Configure App Store Connect Metadata - -### App Information -1. In App Store Connect, go to your app -2. Fill in required information: - - **Subtitle**: Short description (30 chars max) - - **Privacy Policy URL**: Required for Bluetooth apps - - **Category**: Utilities or Productivity - - **Age Rating**: Complete questionnaire - -### App Store Listing -1. Go to **App Store** tab -2. Upload **Screenshots** (required): - - iPhone 6.7" display (1290 x 2796 pixels) - At least 1 screenshot - - iPhone 6.5" display (1242 x 2688 pixels) - At least 1 screenshot - - Optional: iPad screenshots - -3. Fill in **Description**: - ``` - Meshcore Open is a Flutter client for MeshCore LoRa mesh networking devices. - - Features: - - BLE connectivity to MeshCore devices - - Real-time mesh network communication - - Map visualization with OpenStreetMap - - Community management with QR code scanning - - Message tracking and retry system - - Connect to your MeshCore LoRa device and start communicating over the mesh network. - ``` - -4. **Keywords**: `lora,mesh,networking,bluetooth,communication` -5. **Support URL**: Your GitHub or website URL -6. **Marketing URL**: (Optional) - -### Version Information -1. **What's New in This Version**: - ``` - Initial release of Meshcore Open - - - BLE device connectivity - - Mesh network messaging - - Map integration - - Community features - ``` - -2. **Build**: Select the uploaded build once processing completes - -## Step 6: TestFlight Setup - -### Internal Testing (No Review Required) -1. Go to **TestFlight** tab in App Store Connect -2. Click **Internal Testing** → **"+"** to create a group -3. Name your group (e.g., "Internal Testers") -4. Add yourself as a tester using your email -5. Select the build you uploaded -6. Testers will receive an email with TestFlight invitation - -### External Testing (Requires Beta Review) -1. Click **External Testing** → **"+"** to create a group -2. Add build and testers -3. Fill in **Test Information**: - - **What to Test**: Brief description of features - - **Feedback Email**: Your email address -4. Click **Submit for Review** -5. Beta review typically takes 24-48 hours - -## Step 7: App Store Submission - -Once you're ready for public release: - -1. Go to **App Store** tab -2. Complete all required metadata (if not done) -3. Select your build -4. Fill in **App Review Information**: - - **Contact Information**: Your name, phone, email - - **Demo Account**: If app requires login - - **Notes**: Any special instructions for reviewers -5. Answer **Export Compliance** questions: - - Does your app use encryption? **Yes** (uses TLS/HTTPS) - - Is encryption registration required? **No** (standard encryption) -6. Click **Add for Review** -7. Review summary and click **Submit to App Review** - -## Step 8: After Submission - -- **App Review**: Typically 24-48 hours -- **Common Rejection Reasons**: - - Missing privacy policy - - Incomplete app information - - Crashes or bugs - - Misleading app description - -- **If Approved**: You can release immediately or schedule a release date -- **If Rejected**: Address issues and resubmit - -## Updating the App - -When you need to release an update: - -1. **Update version** in `pubspec.yaml`: - ```yaml - version: 0.5.0+6 # Increment version (0.5.0) and build number (+6) - ``` - -2. **Build new IPA**: - ```bash - export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH" - ../flutter/bin/flutter clean - ../flutter/bin/flutter build ipa - ``` - -3. **Upload via Transporter** (same process as above) - -4. **Create new version** in App Store Connect: - - Click **"+"** next to versions - - Select version number - - Update "What's New" text - - Select new build - - Submit for review - -## macOS Build (Bonus) - -To build for macOS: - -```bash -export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH" -../flutter/bin/flutter build macos --release -cd build/macos/Build/Products/Release -zip -r meshcore_open-macos.zip meshcore_open.app -``` - -Distribution: -- Share the zip file directly -- Users unzip and drag to Applications -- First run: Right-click → Open (to bypass Gatekeeper) - -## Troubleshooting - -### Build Errors -- **CocoaPods not found**: Ensure PATH includes `/opt/homebrew/lib/ruby/gems/4.0.0/bin` -- **No signing certificate**: Configure Team in Xcode (Signing & Capabilities) -- **Bundle ID mismatch**: Check `ios/Runner.xcodeproj/project.pbxproj` - -### Upload Errors -- **No profiles found**: Create app in App Store Connect first -- **Bundle ID not registered**: Register in Apple Developer portal -- **Authentication failed**: Use Transporter app instead of CLI - -### TestFlight Issues -- **Build not appearing**: Wait 10-30 minutes for processing -- **Can't add testers**: Check you have available slots (100 internal, 10,000 external) -- **TestFlight crashes**: Check device logs in Xcode → Devices & Simulators - -## Important Files - -- **iOS IPA**: `build/ios/ipa/meshcore_open.ipa` -- **macOS App**: `build/macos/Build/Products/Release/meshcore_open.app` -- **Bundle ID Config**: `ios/Runner.xcodeproj/project.pbxproj` -- **Version Info**: `pubspec.yaml` - -## Useful Links - -- [App Store Connect](https://appstoreconnect.apple.com) -- [Apple Developer Portal](https://developer.apple.com/account) -- [TestFlight Documentation](https://developer.apple.com/testflight/) -- [App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/) -- [Flutter iOS Deployment](https://docs.flutter.dev/deployment/ios) - -## Support - -For issues with: -- **App Store Process**: [Apple Developer Support](https://developer.apple.com/contact/) -- **Flutter Build Issues**: [Flutter GitHub](https://github.com/flutter/flutter/issues) -- **Meshcore Open App**: [GitHub Issues](https://github.com/wel97459/meshcore-open/issues) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index c8028e0..740451b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -16,16 +16,16 @@ if (keystorePropertiesFile.exists()) { android { namespace = "com.meshcore.meshcore_open" compileSdk = flutter.compileSdkVersion - ndkVersion = "29.0.14206865" + ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 isCoreLibraryDesugaringEnabled = true } kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() + jvmTarget = JavaVersion.VERSION_11.toString() } defaultConfig { @@ -83,5 +83,5 @@ flutter { } dependencies { - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4ff626f..43cacc9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -19,7 +19,6 @@ - - - - - - - - - - diff --git a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt index 9022c8b..4350b1e 100644 --- a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt +++ b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt @@ -1,18 +1,5 @@ package com.meshcore.meshcore_open import io.flutter.embedding.android.FlutterActivity -import io.flutter.embedding.engine.FlutterEngine -class MainActivity : FlutterActivity() { - private val usbFunctions by lazy { MeshcoreUsbFunctions(this) } - - override fun configureFlutterEngine(flutterEngine: FlutterEngine) { - super.configureFlutterEngine(flutterEngine) - usbFunctions.configureFlutterEngine(flutterEngine) - } - - override fun onDestroy() { - usbFunctions.dispose() - super.onDestroy() - } -} +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt deleted file mode 100644 index 279ba8a..0000000 --- a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt +++ /dev/null @@ -1,582 +0,0 @@ -package com.meshcore.meshcore_open - -import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.hardware.usb.UsbConstants -import android.hardware.usb.UsbDevice -import android.hardware.usb.UsbDeviceConnection -import android.hardware.usb.UsbEndpoint -import android.hardware.usb.UsbInterface -import android.hardware.usb.UsbManager -import android.os.Build -import android.os.Handler -import android.os.Looper -import io.flutter.embedding.android.FlutterActivity -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import java.util.Locale -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors - -class MeshcoreUsbFunctions( - private val activity: FlutterActivity, -) { - private companion object { - const val usbRecipientInterface = 0x01 - } - - private val usbMethodChannelName = "meshcore_open/android_usb_serial" - private val usbEventChannelName = "meshcore_open/android_usb_serial_events" - private val usbPermissionAction = "com.meshcore.meshcore_open.USB_PERMISSION" - - private val usbManager by lazy { - activity.getSystemService(Context.USB_SERVICE) as UsbManager - } - private val mainHandler = Handler(Looper.getMainLooper()) - private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor() - - @Volatile private var eventSink: EventChannel.EventSink? = null - @Volatile private var usbConnection: UsbDeviceConnection? = null - @Volatile private var usbInEndpoint: UsbEndpoint? = null - @Volatile private var usbOutEndpoint: UsbEndpoint? = null - @Volatile private var controlInterface: UsbInterface? = null - @Volatile private var dataInterface: UsbInterface? = null - private var readThread: Thread? = null - @Volatile private var isReading = false - @Volatile private var connectedDeviceName: String? = null - - private var pendingConnectResult: MethodChannel.Result? = null - private var pendingConnectPortName: String? = null - private var pendingConnectBaudRate: Int = 115200 - - private data class PortConfig( - val controlInterface: UsbInterface?, - val dataInterface: UsbInterface, - val inEndpoint: UsbEndpoint, - val outEndpoint: UsbEndpoint, - ) - - private val permissionReceiver = - object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - when (intent?.action) { - UsbManager.ACTION_USB_DEVICE_DETACHED -> { - handleUsbDetached(intent) - return - } - usbPermissionAction -> Unit - else -> return - } - - val result = pendingConnectResult - val portName = pendingConnectPortName - pendingConnectResult = null - pendingConnectPortName = null - - if (result == null || portName == null) { - return - } - - val device = findUsbDevice(portName) - if (device == null) { - result.error( - "usb_device_missing", - null, - null, - ) - return - } - - val granted = - intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false) - if (!granted || !usbManager.hasPermission(device)) { - result.error("usb_permission_denied", null, null) - return - } - - openUsbDevice(device, pendingConnectBaudRate, result) - } - } - - fun configureFlutterEngine(flutterEngine: FlutterEngine) { - registerUsbPermissionReceiver() - - MethodChannel(flutterEngine.dartExecutor.binaryMessenger, usbMethodChannelName) - .setMethodCallHandler { call, result -> - when (call.method) { - "listPorts" -> result.success(listUsbPorts()) - "connect" -> handleUsbConnect(call, result) - "write" -> handleUsbWrite(call, result) - "disconnect" -> { - scheduleCloseUsbConnection { - result.success(null) - } - } - else -> result.notImplemented() - } - } - - EventChannel(flutterEngine.dartExecutor.binaryMessenger, usbEventChannelName) - .setStreamHandler( - object : EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink) { - eventSink = events - } - - override fun onCancel(arguments: Any?) { - eventSink = null - } - }, - ) - } - - fun dispose() { - closeUsbConnection() - usbIoExecutor.shutdownNow() - try { - activity.unregisterReceiver(permissionReceiver) - } catch (_: IllegalArgumentException) { - } - } - - private fun registerUsbPermissionReceiver() { - val filter = - IntentFilter().apply { - addAction(usbPermissionAction) - addAction(UsbManager.ACTION_USB_DEVICE_DETACHED) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - activity.registerReceiver(permissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED) - } else { - @Suppress("DEPRECATION") - activity.registerReceiver(permissionReceiver, filter) - } - } - - private fun listUsbPorts(): List { - return usbManager.deviceList.values.map { device -> - val productName = device.productName ?: "USB Serial Device" - val vendorProduct = - String.format( - Locale.US, - "VID:%04X PID:%04X", - device.vendorId, - device.productId, - ) - "${device.deviceName} - $productName - $vendorProduct" - } - } - - private fun handleUsbConnect(call: MethodCall, result: MethodChannel.Result) { - val portName = call.argument("portName") - val baudRate = call.argument("baudRate") ?: 115200 - if (portName.isNullOrBlank()) { - result.error("usb_invalid_port", null, null) - return - } - - val device = findUsbDevice(portName) - if (device == null) { - result.error("usb_device_missing", null, null) - return - } - - if (usbManager.hasPermission(device)) { - openUsbDevice(device, baudRate, result) - return - } - - if (pendingConnectResult != null) { - result.error("usb_busy", null, null) - return - } - - pendingConnectResult = result - pendingConnectPortName = portName - pendingConnectBaudRate = baudRate - - val permissionIntent = PendingIntent.getBroadcast( - activity, - 0, - Intent(usbPermissionAction).setPackage(activity.packageName), - pendingIntentFlags(), - ) - usbManager.requestPermission(device, permissionIntent) - } - - private fun handleUsbWrite(call: MethodCall, result: MethodChannel.Result) { - val data = call.argument("data") - val connection = usbConnection - val endpoint = usbOutEndpoint - if (data == null) { - result.error("usb_invalid_data", null, null) - return - } - if (connection == null || endpoint == null) { - result.error("usb_not_connected", null, null) - return - } - - usbIoExecutor.execute { - try { - writeToDevice(data) - mainHandler.post { result.success(null) } - } catch (error: Exception) { - mainHandler.post { - result.error("usb_write_failed", error.message, null) - } - } - } - } - - private fun findUsbDevice(portName: String): UsbDevice? { - val devices = usbManager.deviceList.values - val exactMatch = devices.firstOrNull { it.deviceName == portName } - if (exactMatch != null) { - return exactMatch - } - - val normalizedName = portName.substringBefore(" - ").trim() - return devices.firstOrNull { it.deviceName == normalizedName } - } - - private fun openUsbDevice( - device: UsbDevice, - baudRate: Int, - result: MethodChannel.Result, - ) { - usbIoExecutor.execute { - try { - closeUsbConnection() - - val config = resolvePortConfig(device) - if (config == null) { - mainHandler.post { - result.error( - "usb_driver_missing", - null, - null, - ) - } - return@execute - } - - val connection = usbManager.openDevice(device) - if (connection == null) { - mainHandler.post { - result.error( - "usb_open_failed", - null, - null, - ) - } - return@execute - } - - if (!connection.claimInterface(config.dataInterface, true)) { - connection.close() - mainHandler.post { - result.error( - "usb_open_failed", - null, - null, - ) - } - return@execute - } - - if (config.controlInterface != null && - config.controlInterface.id != config.dataInterface.id && - !connection.claimInterface(config.controlInterface, true) - ) { - connection.releaseInterface(config.dataInterface) - connection.close() - mainHandler.post { - result.error( - "usb_open_failed", - null, - null, - ) - } - return@execute - } - - usbConnection = connection - usbInEndpoint = config.inEndpoint - usbOutEndpoint = config.outEndpoint - controlInterface = config.controlInterface - dataInterface = config.dataInterface - - configureDevice(connection, config, baudRate) - - connectedDeviceName = device.deviceName - startReadLoop() - - mainHandler.post { - result.success(null) - } - } catch (error: Exception) { - closeUsbConnection() - mainHandler.post { - result.error("usb_connect_failed", error.message, null) - } - } - } - } - - private fun resolvePortConfig(device: UsbDevice): PortConfig? { - var preferredDataInterface: UsbInterface? = null - var preferredInEndpoint: UsbEndpoint? = null - var preferredOutEndpoint: UsbEndpoint? = null - var fallbackDataInterface: UsbInterface? = null - var fallbackInEndpoint: UsbEndpoint? = null - var fallbackOutEndpoint: UsbEndpoint? = null - var preferredControlInterface: UsbInterface? = null - - for (interfaceIndex in 0 until device.interfaceCount) { - val usbInterface = device.getInterface(interfaceIndex) - var inEndpoint: UsbEndpoint? = null - var outEndpoint: UsbEndpoint? = null - - for (endpointIndex in 0 until usbInterface.endpointCount) { - val endpoint = usbInterface.getEndpoint(endpointIndex) - if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) { - continue - } - when (endpoint.direction) { - UsbConstants.USB_DIR_IN -> if (inEndpoint == null) inEndpoint = endpoint - UsbConstants.USB_DIR_OUT -> if (outEndpoint == null) outEndpoint = endpoint - } - } - - val hasDataPair = inEndpoint != null && outEndpoint != null - when { - usbInterface.interfaceClass == UsbConstants.USB_CLASS_COMM && - preferredControlInterface == null -> { - preferredControlInterface = usbInterface - } - hasDataPair && - usbInterface.interfaceClass == UsbConstants.USB_CLASS_CDC_DATA -> { - preferredDataInterface = usbInterface - preferredInEndpoint = inEndpoint - preferredOutEndpoint = outEndpoint - } - hasDataPair && fallbackDataInterface == null -> { - fallbackDataInterface = usbInterface - fallbackInEndpoint = inEndpoint - fallbackOutEndpoint = outEndpoint - } - } - } - - val dataInterface = preferredDataInterface ?: fallbackDataInterface ?: return null - val inEndpoint = preferredInEndpoint ?: fallbackInEndpoint ?: return null - val outEndpoint = preferredOutEndpoint ?: fallbackOutEndpoint ?: return null - return PortConfig(preferredControlInterface, dataInterface, inEndpoint, outEndpoint) - } - - private fun configureDevice( - connection: UsbDeviceConnection, - config: PortConfig, - baudRate: Int, - ) { - val control = config.controlInterface ?: return - val lineCoding = - byteArrayOf( - (baudRate and 0xFF).toByte(), - ((baudRate shr 8) and 0xFF).toByte(), - ((baudRate shr 16) and 0xFF).toByte(), - ((baudRate shr 24) and 0xFF).toByte(), - 0, // stop bits: 1 - 0, // parity: none - 8, // data bits - ) - - val lineCodingResult = - connection.controlTransfer( - UsbConstants.USB_DIR_OUT or - UsbConstants.USB_TYPE_CLASS or - usbRecipientInterface, - 0x20, - 0, - control.id, - lineCoding, - lineCoding.size, - 1000, - ) - if (lineCodingResult < 0) { - throw IllegalStateException("Failed to configure USB line coding") - } - - val controlLineResult = - connection.controlTransfer( - UsbConstants.USB_DIR_OUT or - UsbConstants.USB_TYPE_CLASS or - usbRecipientInterface, - 0x22, - 0x0001, // DTR on, RTS off - control.id, - null, - 0, - 1000, - ) - if (controlLineResult < 0) { - throw IllegalStateException("Failed to configure USB control line state") - } - } - - private fun startReadLoop() { - val connection = usbConnection ?: return - val endpoint = usbInEndpoint ?: return - - isReading = true - readThread = - Thread({ - val packetSize = endpoint.maxPacketSize.coerceAtLeast(64) - val buffer = ByteArray(packetSize * 4) - try { - while (isReading) { - val bytesRead = connection.bulkTransfer(endpoint, buffer, buffer.size, 250) - if (!isReading) { - break - } - if (bytesRead <= 0) { - continue - } - val packet = buffer.copyOf(bytesRead) - mainHandler.post { - eventSink?.success(packet) - } - } - } catch (error: Exception) { - if (isReading) { - mainHandler.post { - eventSink?.error( - "usb_io_error", - error.message ?: "USB serial I/O error", - null, - ) - } - scheduleCloseUsbConnection() - } - } - }, "MeshCoreUsbRead").also { thread -> - thread.isDaemon = true - thread.start() - } - } - - private fun writeToDevice(data: ByteArray) { - val connection = usbConnection ?: throw IllegalStateException("USB connection missing") - val endpoint = usbOutEndpoint ?: throw IllegalStateException("USB output endpoint missing") - var offset = 0 - val maxPacketSize = endpoint.maxPacketSize.coerceAtLeast(64) - while (offset < data.size) { - val chunkSize = minOf(maxPacketSize, data.size - offset) - val chunk = data.copyOfRange(offset, offset + chunkSize) - val bytesWritten = connection.bulkTransfer(endpoint, chunk, chunkSize, 1000) - if (bytesWritten != chunkSize) { - throw IllegalStateException("Short USB write: wrote $bytesWritten of $chunkSize bytes") - } - offset += chunkSize - } - } - - private fun scheduleCloseUsbConnection(onComplete: (() -> Unit)? = null) { - usbIoExecutor.execute { - closeUsbConnection() - if (onComplete != null) { - mainHandler.post(onComplete) - } - } - } - - @Synchronized - private fun closeUsbConnection() { - isReading = false - readThread?.interrupt() - if (readThread != null && readThread !== Thread.currentThread()) { - try { - readThread?.join(300) - } catch (_: InterruptedException) { - Thread.currentThread().interrupt() - } - } - readThread = null - - val connection = usbConnection - val claimedControl = controlInterface - val claimedData = dataInterface - - usbInEndpoint = null - usbOutEndpoint = null - controlInterface = null - dataInterface = null - usbConnection = null - - if (connection != null) { - if (claimedControl != null) { - try { - connection.releaseInterface(claimedControl) - } catch (_: Exception) { - } - } - if (claimedData != null && claimedData.id != claimedControl?.id) { - try { - connection.releaseInterface(claimedData) - } catch (_: Exception) { - } - } - try { - connection.close() - } catch (_: Exception) { - } - } - connectedDeviceName = null - } - - private fun handleUsbDetached(intent: Intent) { - val detachedDevice = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java) - } else { - @Suppress("DEPRECATION") - intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) - } - - val detachedName = detachedDevice?.deviceName ?: return - - if (pendingConnectPortName == detachedName) { - pendingConnectResult?.error( - "usb_device_detached", - "USB device was removed before the connection completed", - null, - ) - pendingConnectResult = null - pendingConnectPortName = null - } - - if (connectedDeviceName == detachedName) { - scheduleCloseUsbConnection { - eventSink?.error( - "usb_device_detached", - "USB device was disconnected", - null, - ) - } - } - } - - private fun pendingIntentFlags(): Int { - var flags = PendingIntent.FLAG_UPDATE_CURRENT - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - flags = flags or PendingIntent.FLAG_MUTABLE - } - return flags - } -} diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html deleted file mode 100644 index 2220133..0000000 --- a/android/build/reports/problems/problems-report.html +++ /dev/null @@ -1,663 +0,0 @@ - - - - - - - - - - - - - Gradle Configuration Cache - - - -
- -
- Loading... -
- - - - - - diff --git a/assets/badges/badge_obtainium.png b/assets/badges/badge_obtainium.png deleted file mode 100644 index cc3a0ed..0000000 Binary files a/assets/badges/badge_obtainium.png and /dev/null differ diff --git a/assets/icons/done_all.svg b/assets/icons/done_all.svg deleted file mode 100644 index bfeeec0..0000000 --- a/assets/icons/done_all.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/BLE_PROTOCOL.md b/docs/BLE_PROTOCOL.md index c17c3e7..993c3ea 100644 --- a/docs/BLE_PROTOCOL.md +++ b/docs/BLE_PROTOCOL.md @@ -21,12 +21,7 @@ The MeshCore BLE protocol implements a binary frame-based communication system u ### Connection Flow -1. **Scan** for devices with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`): - - `MeshCore-` - - `Whisper-` - - `WisCore-` - - `HT-` - - `LowMesh_MC_` +1. **Scan** for devices with name prefix `MeshCore-` 2. **Connect** with 15-second timeout 3. **Request MTU** of 185 bytes (falls back to default if unsupported) 4. **Discover services** and locate NUS characteristics diff --git a/docs/screenshots/signal-ui-consistency.png b/docs/screenshots/signal-ui-consistency.png deleted file mode 100644 index 2575945..0000000 Binary files a/docs/screenshots/signal-ui-consistency.png and /dev/null differ diff --git a/documentation/README.md b/documentation/README.md deleted file mode 100644 index 1367013..0000000 --- a/documentation/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# MeshCore Open - Feature Documentation - -MeshCore Open is an open-source Flutter client for MeshCore LoRa mesh networking devices. This documentation covers every user-facing feature, how to access it, and what it does. - -## Table of Contents - -1. [Scanner & Connection](scanner-and-connection.md) - BLE scanning, USB serial, and TCP connection -2. [Navigation](navigation.md) - App flow, device screen, and quick-switch navigation -3. [Contacts](contacts.md) - Contact management, groups, discovery, and sharing -4. [Chat & Messaging](chat-and-messaging.md) - Direct messages, message status, reactions, and retries -5. [Channels](channels.md) - Broadcast channels, communities, and channel chat -6. [Map & Location](map-and-location.md) - Node map, path tracing, line-of-sight, and offline caching -7. [Settings](settings.md) - Device settings, app settings, radio configuration, and exports -8. [Notifications](notifications.md) - System notifications, unread badges, and notification preferences -9. [Repeater Management](repeater-management.md) - Repeater hub, status, CLI, telemetry, and neighbors -10. [Additional Features](additional-features.md) - GIF picker, localization, debug logs, SMAZ compression, and more -11. [BLE Protocol & Data Layer](ble-protocol.md) - Technical reference for the communication protocol and data architecture - -## App Overview - -MeshCore Open connects to MeshCore LoRa mesh radios over BLE, USB, or TCP. Once connected, users can: - -- **Chat** with other mesh nodes via encrypted direct messages -- **Broadcast** on shared channels (public, hashtag, private, or community-scoped) -- **View nodes on a map** with GPS locations, predicted positions, and path traces -- **Manage repeaters** with CLI access, telemetry, neighbor info, and settings -- **Share contacts** via `meshcore://` URIs and QR codes -- **Configure radio settings** including frequency, power, bandwidth, and spreading factor -- **Cache offline maps** for use without internet connectivity -- **Analyze line-of-sight** between nodes with terrain elevation profiles diff --git a/documentation/additional-features.md b/documentation/additional-features.md deleted file mode 100644 index f7b8319..0000000 --- a/documentation/additional-features.md +++ /dev/null @@ -1,187 +0,0 @@ -# Additional Features - -## GIF Picker (Giphy Integration) - -### How to Access -In any chat screen (direct or channel), tap the GIF button in the message input bar. - -### What the User Sees -A bottom sheet with a search field and a grid of GIF thumbnails. - -### Key Interactions -- On open, loads trending GIFs (G-rated, 25 results) -- Type to search and press the keyboard submit button (search triggers on submit, not on each keystroke). Clearing the search field reloads trending GIFs -- On network/API errors, a "Retry" button is shown in-place -- Tap a GIF to select it — the chat input shows an inline preview with an X button to dismiss -- Send the message to transmit the GIF reference (`g:`) -- Recipients see the GIF rendered inline via Giphy CDN -- "Powered by Giphy" attribution is always shown at the bottom of the picker -- The bottom sheet occupies 70% of screen height - ---- - -## Localization / Multi-Language Support - -### How to Access -App Settings → Appearance → Language - -### Supported Languages (15) -English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian - -### How It Works -- All UI strings go through Flutter's ARB localization system -- Language can follow the system locale or be explicitly overridden -- Changes take effect immediately - ---- - -## Discovered Contacts Screen - -### How to Access -From Contacts screen → overflow menu → "Discovered Contacts" - -### What the User Sees -A list of nodes heard passively over the air but not yet added as contacts. Each shows: -- Color-coded avatar (by type) -- Name -- Short public key -- Last-seen time - -### Key Interactions -- Search bar with debounced filtering -- Sort by last seen or name; filter by type -- **Tap**: Import the contact (adds to your contact list) -- **Long-press**: Add Contact, Copy `meshcore://` URI to clipboard, or Delete -- Overflow menu → "Delete All" (with confirmation) -- Already-known contacts and your own node are filtered out - ---- - -## SMAZ Compression - -### What It Is -An optional per-contact and per-channel text compression feature using the SMAZ algorithm (optimized for short English text). - -### How to Enable -- **Per contact**: Chat screen → info button → toggle "SMAZ compression" -- **Per channel**: Long-press channel → Edit → toggle "SMAZ compression" - -### How It Works -- When enabled, compression is applied using a "compress only if smaller" strategy — the message is only transmitted compressed if the encoded result is actually shorter than the original. Otherwise, the original text is sent uncompressed -- Compressed messages are transmitted with a `s:` prefix followed by base64-encoded data -- Recipients using MeshCore Open will decompress automatically. **Recipients using other software** that is not SMAZ-aware will see garbled `s:...` text -- The codec operates on ASCII. Non-ASCII / non-English text generally does not benefit from compression and may even expand. Best suited for short English messages -- Disabled by default - ---- - -## Community QR Scanner - -### How to Access -From Channels screen → "+" FAB → "Scan Community QR" - -### What the User Sees -A live QR scanner view with instruction text overlay. - -### Key Interactions -- Scan a community QR code shared by another member -- On valid scan: confirmation dialog showing community name and ID -- Option to "Add public channel to device" on join -- If already a member: shows an "Already a member" dialog -- Invalid QR: shows an orange error snackbar - ---- - -## Channel Message Path Viewing - -### How to Access -In a channel chat, tap a message bubble (mobile) or use the "Path" action (desktop). - -### What the User Sees -- Summary card: sender, time, repeat count, path type, observed hops -- "Other Observed Paths" section (if multiple paths detected) -- "Repeater Hops" section listing each hop with hex prefix, resolved name, and GPS coordinates - -### Actions -- **Radar icon**: Opens path trace map for live trace -- **Map icon**: Opens a map with hop markers and polyline -- **Path dropdown**: Switch between observed path variants (if multiple) - ---- - -## Debug Logging - -### BLE Debug Log -**Access**: Settings → BLE Debug Log - -Two views: -- **Frames**: Each BLE frame with direction, description, hex preview, timestamp. Long-press to copy hex. -- **Raw Log RX**: Decoded LoRa packets with route type, payload type, path bytes, and summary. - -### App Debug Log -**Access**: Settings → App Debug Log (must be enabled first in App Settings → Debug) - -Structured log entries with level (Info/Warning/Error), tag, message, and timestamp. - -Both logs support copy-all and clear operations. - ---- - -## Chrome Required Screen - -### When It Appears -Automatically shown on web platforms when a non-Chromium browser is detected. - -### What the User Sees -A full-screen informational page explaining that Web Bluetooth requires a Chromium-based browser. No interactive elements — purely informational. - ---- - -## Path History Service - -### What It Does (Background Service) -Maintains an in-memory LRU cache of up to 50 contacts, each with up to 100 route history entries, tracking: -- Hop count and trip time -- Success/failure counts and route weights -- Flood vs. direct discovery - -### Path Scoring -Paths are scored using a weighted formula: reliability (45%), route weight (20%), latency (25%), and freshness (10%). These weights are internal and not user-configurable. Paths whose weight drops to zero or below are automatically deleted. Flood deliveries that receive an ACK give a weight boost (+0.5) to the specific return path. - -Used internally for: -- **Auto route rotation**: Cycles through known paths using configurable weights on retries, with a diversity window to avoid re-using recently tried paths -- **Path selection**: Picks the best-scored path for each retry attempt -- **Flood statistics**: Tracks flood vs. direct discovery ratios - ---- - -## Message Retry Service - -### What It Does (Background Service) -Handles reliable delivery of outgoing direct messages: -1. Assigns a UUID and sends immediately. Only one message per contact can be in-flight at a time (avoids overflowing the firmware's 8-entry ACK table); subsequent messages are queued -2. Listens for ACK frames matched via SHA-256 hash of `[timestamp][attempt][text][sender_pubkey]` -3. On timeout, retries with exponential backoff: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s...) -4. Each retry may use a different path (via path history diversity window) -5. After max retries: marks failed but keeps a **30-second grace window** during which a late ACK can still resolve the message to "delivered". Optionally clears the contact's path -6. Reports RTT and path data for quality learning -7. Maintains an ACK hash history (last 50 entries) to handle duplicate ACKs - -### Configurable Settings (App Settings → Messaging) -- Max retries (2–10, default 5) -- Clear path on max retry (on/off) -- Auto route rotation with weight parameters - ---- - -## Timeout Prediction (ML) - -### What It Does (Background Service) -An ML-based service that predicts expected delivery timeouts: -- Collects delivery observations (path length, message size, time since last RX, delivery time) in a sliding window of up to 100 observations (oldest evicted first) -- Requires **10 minimum observations** before first training. After that, retrains every 5 new observations -- Applies a **1.5x safety margin** to raw predictions (the actual timeout issued is 1.5× the model's predicted delivery time) -- Features with zero variance are automatically excluded from training -- Blends per-contact statistics with ML predictions -- Falls back to `3000 + 3000 × pathLength` ms when insufficient data -- Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost) diff --git a/documentation/ble-protocol.md b/documentation/ble-protocol.md deleted file mode 100644 index ec24094..0000000 --- a/documentation/ble-protocol.md +++ /dev/null @@ -1,254 +0,0 @@ -# BLE Protocol & Data Layer - -This is a technical reference for the communication protocol and data architecture. - -## Transport Layer - -The app supports three transports, all sharing the same command/response protocol: - -| Transport | Method | Implementation | -|---|---|---| -| Bluetooth LE | Nordic UART Service (NUS) GATT | `flutter_blue_plus` | -| USB Serial | Packet-framed serial | `MeshCoreUsbManager` | -| TCP | Packet-framed socket | `MeshCoreTcpConnector` | - -### BLE (Nordic UART Service) - -- **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e` -- **RX Characteristic** (write to device): `6e400002-b5a3-f393-e0a9-e50e24dcca9e` -- **TX Characteristic** (notify from device): `6e400003-b5a3-f393-e0a9-e50e24dcca9e` - -Raw `Uint8List` payloads are written directly to the RX characteristic. Writes use "write without response" if supported, falling back to "write with response". - -### USB and TCP Framing - -Both use a lightweight packet framing codec: - -``` -TX (host → device): [0x3C][len_lo][len_hi][payload...] -RX (device → host): [0x3E][len_lo][len_hi][payload...] -``` - -- Frame start: `0x3C` (`<`) for outgoing, `0x3E` (`>`) for incoming -- Length: 2-byte little-endian, payload only -- Max payload: 172 bytes -- TCP: `tcpNoDelay: true` (Nagle disabled), writes serialized to prevent interleaving -- USB: 10ms post-write delay between frames - -## Connection State Machine - -``` -enum MeshCoreConnectionState { - disconnected, - scanning, - connecting, - connected, - disconnecting, -} -``` - -## BLE Connection Lifecycle - -1. **Scan** with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`): - - `MeshCore-` - - `Whisper-` - - `WisCore-` - - `HT-` - - `LowMesh_MC_` -2. **Connect** with 15-second timeout -3. **Request MTU** 185 bytes (non-web only) -4. **Discover services** and locate NUS -5. **Enable TX notifications** (up to 3 attempts on native) -6. **Subscribe** to TX characteristic for incoming frames -7. **Initial sync**: device info query, time sync, channel sync - -## Auto-Reconnect (BLE Only) - -On unexpected disconnection, auto-reconnect with exponential backoff: -- Delays: 1s, 2s, 4s, 8s, 16s, 30s, 30s... -- Resets on successful connection -- Disabled for manual disconnects -- Not available for USB or TCP - -## Protocol Constants - -| Constant | Value | Description | -|---|---|---| -| Max frame size | 172 bytes | BLE/USB/TCP payload limit | -| Public key size | 32 bytes | Ed25519 public key | -| Max path size | 64 bytes | Maximum path data | -| Max name size | 32 bytes | Maximum node name | -| Max text payload | 160 bytes | Firmware `MAX_TEXT_LEN` | -| App protocol version | 3 | Sent in device query | -| Contact frame size | 148 bytes | Fixed-size contact record | - -## Command Codes (App → Device) - -| Code | Name | Description | -|------|------|-------------| -| 1 | CMD_APP_START | Announce app connection | -| 2 | CMD_SEND_TXT_MSG | Send direct text message | -| 3 | CMD_SEND_CHANNEL_TXT_MSG | Send channel text message | -| 4 | CMD_GET_CONTACTS | Request contact list | -| 5 | CMD_GET_DEVICE_TIME | Query device clock | -| 6 | CMD_SET_DEVICE_TIME | Set device clock | -| 7 | CMD_SEND_SELF_ADVERT | Broadcast own advertisement | -| 8 | CMD_SET_ADVERT_NAME | Set node name | -| 9 | CMD_ADD_UPDATE_CONTACT | Add or update a contact | -| 10 | CMD_SYNC_NEXT_MESSAGE | Request next queued message | -| 11 | CMD_SET_RADIO_PARAMS | Set radio parameters | -| 12 | CMD_SET_RADIO_TX_POWER | Set TX power | -| 13 | CMD_RESET_PATH | Reset contact path | -| 14 | CMD_SET_ADVERT_LATLON | Set advertised location | -| 15 | CMD_REMOVE_CONTACT | Remove a contact | -| 16 | CMD_SHARE_CONTACT | Share contact to mesh | -| 17 | CMD_EXPORT_CONTACT | Export contact as bytes | -| 18 | CMD_IMPORT_CONTACT | Import contact from bytes | -| 19 | CMD_REBOOT | Reboot device | -| 20 | CMD_GET_BATT_AND_STORAGE | Query battery and storage | -| 22 | CMD_DEVICE_QUERY | Query device info | -| 26 | CMD_SEND_LOGIN | Login to repeater/room | -| 27 | CMD_SEND_STATUS_REQ | Request repeater status | -| 30 | CMD_GET_CONTACT_BY_KEY | Get contact by public key | -| 31 | CMD_GET_CHANNEL | Get channel definition | -| 32 | CMD_SET_CHANNEL | Set channel name and PSK | -| 36 | CMD_SEND_TRACE_PATH | Request path trace | -| 38 | CMD_SET_OTHER_PARAMS | Set misc parameters | -| 39 | CMD_GET_TELEMETRY_REQ | Request sensor telemetry | -| 40 | CMD_GET_CUSTOM_VAR | Get custom variables | -| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable | -| 50 | CMD_SEND_BINARY_REQ | Send binary request | -| 57 | CMD_SEND_ANON_REQ | Send anonymous request | -| 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration | -| 59 | CMD_GET_AUTO_ADD_CONFIG | Get auto-add configuration | - -## Response / Push Codes (Device → App) - -| Code | Name | Description | -|------|------|-------------| -| 0 | RESP_CODE_OK | Generic success | -| 1 | RESP_CODE_ERR | Generic error | -| 2 | RESP_CODE_CONTACTS_START | Contact list begins | -| 3 | RESP_CODE_CONTACT | Single contact data | -| 4 | RESP_CODE_END_OF_CONTACTS | Contact list complete | -| 5 | RESP_CODE_SELF_INFO | Device self-info response | -| 6 | RESP_CODE_SENT | Message transmitted; carries `[1]=is_flood, [2–5]=ack_hash, [6–9]=estimated_timeout_ms` | -| 7 | RESP_CODE_CONTACT_MSG_RECV | Incoming direct message (v2) | -| 8 | RESP_CODE_CHANNEL_MSG_RECV | Incoming channel message (v2) | -| 10 | RESP_CODE_NO_MORE_MESSAGES | No more queued messages | -| 11 | RESP_CODE_EXPORT_CONTACT | Exported contact data | -| 9 | RESP_CODE_CURR_TIME | Current device time | -| 12 | RESP_CODE_BATT_AND_STORAGE | Battery mV (uint16 LE) + storage used/total (uint32 LE each) | -| 13 | RESP_CODE_DEVICE_INFO | Firmware info | -| 16 | RESP_CODE_CONTACT_MSG_RECV_V3 | Incoming direct message (v3) | -| 17 | RESP_CODE_CHANNEL_MSG_RECV_V3 | Incoming channel message (v3) | -| 18 | RESP_CODE_CHANNEL_INFO | Channel definition | -| 21 | RESP_CODE_CUSTOM_VARS | Custom variables | -| 25 | RESP_CODE_AUTO_ADD_CONFIG | Auto-add flags | -| 0x80 | PUSH_CODE_ADVERT | Known contact re-seen | -| 0x81 | PUSH_CODE_PATH_UPDATED | Better path found; carries the 32-byte public key of the updated contact | -| 0x82 | PUSH_CODE_SEND_CONFIRMED | Delivery ACK from remote; carries ACK hash (4 bytes) + trip time (4 bytes) | -| 0x83 | PUSH_CODE_MSG_WAITING | Offline messages queued | -| 0x85 | PUSH_CODE_LOGIN_SUCCESS | Repeater/room login succeeded | -| 0x86 | PUSH_CODE_LOGIN_FAIL | Repeater/room login failed | -| 0x87 | PUSH_CODE_STATUS_RESPONSE | Repeater status response | -| 0x88 | PUSH_CODE_LOG_RX_DATA | Radio RX data with SNR (int8, units 1/4 dB), RSSI, and raw radio packet | -| 0x89 | PUSH_CODE_TRACE_DATA | Path trace result | -| 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered | -| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data | -| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response | - -## Data Models - -### Contact -32-byte public key (primary identity), name, type (chat/repeater/room/sensor), flags, path data, GPS coordinates, last-seen timestamp. Parsed from 148-byte firmware frames with this layout: - -``` -[0] = resp_code -[1–32] = public key (32 bytes) -[33] = type (1=chat, 2=repeater, 3=room, 4=sensor) -[34] = flags (bit 0 = favorite) -[35] = path_length -[36–99] = path (64 bytes) -[100–131] = name (32 bytes, null-padded) -[132–135] = timestamp (uint32 LE) -[136–139] = latitude (int32 LE, × 1e-6 degrees) -[140–143] = longitude (int32 LE, × 1e-6 degrees) -[144–147] = last_modified (uint32 LE) -``` - -### Message (Direct) -Sender key, text, timestamp, outgoing flag, status (pending/sent/delivered/failed), message ID (UUID), retry count, ACK hash, trip time, path data, reactions. - -### Channel Message -Sender name, text, timestamp, status (pending/sent/failed), repeater hops, path variants, channel index, reactions, reply threading fields. - -### Channel -Index (0–7), name, 16-byte PSK, unread count. PSK derivation methods for hashtag (SHA-256) and community (HMAC-SHA256) channels. - -### Community -UUID, name, 32-byte secret, hashtag channel list. Shared via QR code. - -## Persistence - -All data is stored via `SharedPreferences` (JSON-serialized). No SQLite or other database. - -| Data | Storage Key Pattern | Scope | -|---|---|---| -| Contacts | `contacts` | Per device identity | -| Messages | `messages_` | Per device + contact | -| Channel Messages | `channel_messages_` | Per device + channel | -| Channels | `channels` | Per device identity | -| Channel Order | `channel_order_` | Per device identity | -| Contact Groups | `contact_groups` | Per device identity | -| Communities | `communities_v1` | Per device identity | -| Unread Counts | `contact_unread_count` | Per device identity | -| Discovered Contacts | `discovered_contacts` | Global | -| App Settings | `app_settings` | Global | -| Path History | `path_history_` | Per contact | - -## Auto-Add Configuration Bitmask - -Used by `CMD_SET_AUTO_ADD_CONFIG` (58) and `RESP_CODE_AUTO_ADD_CONFIG` (25): - -| Bit | Flag | Description | -|-----|------|-------------| -| 0 | 0x01 | Overwrite oldest contact when list is full | -| 1 | 0x02 | Auto-add chat users | -| 2 | 0x04 | Auto-add repeaters | -| 3 | 0x08 | Auto-add room servers | -| 4 | 0x10 | Auto-add sensors | - -## Radio Packet Payload Types - -Seen inside `PUSH_CODE_LOG_RX_DATA` raw packets: - -| Code | Type | -|------|------| -| 0x00 | REQ (request) | -| 0x01 | RESPONSE | -| 0x02 | TXTMSG (text message) | -| 0x03 | ACK | -| 0x04 | ADVERT | -| 0x05 | GRPTXT (group/channel text) | -| 0x06 | GRPDATA (group data) | -| 0x07 | ANONREQ (anonymous request) | -| 0x08 | PATH | -| 0x09 | TRACE | -| 0x0A | MULTIPART | -| 0x0B | CONTROL | -| 0x0F | RAW_CUSTOM | - -## State Management - -Uses Flutter `Provider` with `ChangeNotifier`. The central state holder is `MeshCoreConnector`, which owns all in-memory collections and fires debounced (50ms) `notifyListeners()` to update the UI. In-memory conversations are windowed to 200 messages per contact; older messages remain on disk and are loaded on demand. - -### Data Flow - -1. Raw frames arrive over BLE/USB/TCP -2. First byte is parsed as response/push code -3. Appropriate model factory (`fromFrame()`) parses the data -4. In-memory collections are updated -5. Storage stores are persisted (async) -6. `notifyListeners()` triggers UI rebuilds -7. Screens read current state via getters diff --git a/documentation/channels.md b/documentation/channels.md deleted file mode 100644 index 21fb52e..0000000 --- a/documentation/channels.md +++ /dev/null @@ -1,164 +0,0 @@ -# Channels - -## Overview - -Channels are broadcast group-chat spaces secured by a 16-byte pre-shared key (PSK). Any device with the same channel index and PSK will receive and decrypt channel messages. Unlike direct messages, channel messages are broadcast to the entire mesh. - -Up to 8 channels (indices 0–7) can be active simultaneously on one device. - -## How to Access - -QuickSwitchBar tab 1 (middle) from any main screen. - -## Channel Types - -| Type | Icon | Color | Description | -|---|---|---|---| -| Public | Globe | Green | Fixed well-known PSK; any device can join | -| Hashtag | Hash tag | Blue | PSK derived from the hashtag name via SHA-256; discoverable by convention | -| Private | Lock | Blue | Random PSK; requires out-of-band sharing of the 32-hex key | -| Community | Groups/Tag | Purple | PSK derived via HMAC-SHA256 from a community's shared secret | - -## Channels List Screen - -### What the User Sees - -- **Search bar** with live text filtering (300ms debounce) -- **Sort/filter button** -- **Scrollable list of channel cards**, each showing: - - Type icon with color coding (purple badge overlay for community channels) - - Channel name (or "Channel N" if unnamed) - - Subtitle: "Public channel", "Hashtag channel", "Private channel", or "Community channel - {name}" - - Unread badge (if messages are unread) - - Drag handle (when manual sort is active) -- **"+" FAB** to add a new channel -- **Overflow menu**: Disconnect, Manage Communities (only shown when at least one community exists), Settings - -If no channels exist, an empty state with an "Add Public Channel" shortcut is shown. If a search produces no results, a separate "no results" empty state with a search-off icon is shown. - -Pull-to-refresh (swipe down) forces a re-fetch of channels from the device firmware. - -### Sorting Options - -- **Manual** (default): Drag-and-drop reordering, persisted (drag handles are hidden when a search query is active) -- **A–Z**: Alphabetical -- **Latest messages**: Most recent first -- **Unread**: Most unread first - -## Adding a Channel - -Tap the "+" FAB to open a dialog with six options: - -1. **Create Private Channel** — Enter a name (max 31 characters); a random PSK is generated -2. **Join Private Channel** — Enter a name and a 32-hex PSK (non-hex characters like spaces and dashes are silently stripped, so pasted keys with formatting are accepted) -3. **Join Public Channel** — One tap; uses the well-known public PSK (only shown if no public channel exists) -4. **Join Hashtag Channel** — Enter a hashtag name; PSK is derived from the name. If communities exist, choose between regular hashtag (SHA-256) or community hashtag (HMAC) -5. **Scan Community QR** — Opens QR scanner to join a community -6. **Create Community** — Enter a name; generates a random 32-byte secret; optionally adds a community public channel; shows QR code for sharing - -## Channel Actions (Long-Press / Right-Click) - -| Action | Description | -|---|---| -| Edit | Change name, PSK (with a dice icon to generate a random PSK), or SMAZ compression toggle (compresses outgoing messages to allow longer text within the byte limit) | -| Mute / Unmute | Toggle push notification suppression for this channel | -| Delete | Remove the channel from the device (confirmation required) | - -## Channel Chat - -Tap a channel card to open the channel chat screen. - -### App Bar - -- Type icon (public/private/hashtag) -- Channel name -- Subtitle: "{type} - {N} unread" - -### Message Display - -- Reverse-scrolling list (newest at bottom) -- **Incoming messages**: Colored avatar with sender's initial (or first emoji if name starts with one; color is deterministic from sender name hash), sender name in primary color, message bubble -- **Outgoing messages**: Primary container color bubble with a small status icon: pending (clock), sent (checkmark), or failed (red error circle) -- Automatic older-message loading on scroll-to-top -- Jump-to-bottom button when scrolled up -- **Pinch-to-zoom**: Two-finger zoom (0.8x–1.8x) and double-tap to reset text size -- **Message tracing mode** (when enabled in App Settings): Each bubble additionally shows path prefix bytes (`via XX,YY,...`), a timestamp, and a repeat count icon - -### Message Types in Chat - -- **Plain text** with linkified URLs -- **GIFs** (`g:{gifId}`) rendered inline via Giphy CDN -- **Location pins** (`m:{lat},{lon}|{label}|`) shown as tappable location cards -- **Reactions** displayed as emoji pills below target messages - -### Replies (Channel Chat Only) - -- **Mobile**: Swipe an **incoming** message left to trigger reply (with haptic feedback). You cannot swipe your own outgoing messages. Swipe reply is not available on desktop. -- **All platforms**: Long-press → "Reply" -- Reply banner appears above the input bar with the quoted message (tap X to cancel) -- Sent replies are prefixed `@[{senderName}] {text}` -- Received replies show a bordered quote block inside the bubble; tapping scrolls to the original. Reply previews render GIF thumbnails and location pin icons, not just text. - -### Message Path Viewing - -- **Mobile**: Tap a message bubble to view its routing path -- **Desktop**: Long-press/right-click → "Path" (tapping the bubble does nothing on desktop) -- Opens the Channel Message Path Screen (see [Additional Features](additional-features.md)) - -### Context Actions (Long-Press / Right-Click) - -| Action | Availability | Description | -|---|---|---| -| Reply | All messages | Triggers reply mode | -| Path | Desktop only | Opens message path view | -| Add Reaction | Incoming messages only | Opens emoji picker (cannot react to your own messages) | -| Copy | All messages | Copies text to clipboard | -| Delete | All messages | Removes locally (not from mesh) | - -### Message Path Viewing - -Tap a message bubble to open the Channel Message Path Screen, which shows: -- Each hop in the path as a visual chain -- Known contacts identified by name at each hop -- Observed vs. declared hop counts -- Alternative path variants (if received via multiple paths) -- Map view buttons for geographic path visualization - -## Communities - -Communities are a layer above channels that provide a private namespace. - -### What is a Community? - -A community has a name and a 32-byte random secret. Channel PSKs are derived from this secret: -- **Public channel**: `HMAC-SHA256(secret, "channel:v1:__public__")[:16]` -- **Hashtag channel**: `HMAC-SHA256(secret, "channel:v1:{hashtag}")[:16]` - -Outsiders who don't know the secret cannot discover or join community channels. - -### Sharing a Community - -Communities are shared via QR codes containing a JSON payload: -```json -{"v": 1, "type": "meshcore_community", "name": "...", "k": ""} -``` - -### Managing Communities - -From the channels screen overflow menu → "Manage Communities". Opens a draggable scrollable sheet (resizable 30–90% of screen height): - -- Each community shows its name and a short community ID (first 8 hex characters) -- **Tap a community** to directly show its QR code for sharing -- **Popup menu** per community: - - **Show QR** — displays the QR code for sharing with new members - - **Delete** — removes the community locally and deletes all associated device channels (confirmation dialog warns how many channels will be removed) - -## How Channels Differ from Direct Messages - -| Aspect | Channels | Direct Messages | -|---|---|---| -| Addressing | Broadcast to all nodes with matching PSK | Point-to-point to a specific contact | -| Encryption | Shared PSK (symmetric) | Contact's public key (asymmetric) | -| Sender identity | Plain text prefix in payload | Verified via public key | -| Replies | Supported (swipe or long-press) | Not supported | -| Retry mechanism | No automatic retry | Exponential backoff with path rotation | diff --git a/documentation/chat-and-messaging.md b/documentation/chat-and-messaging.md deleted file mode 100644 index 22030d5..0000000 --- a/documentation/chat-and-messaging.md +++ /dev/null @@ -1,120 +0,0 @@ -# Chat & Messaging - -## Overview - -The app supports two chat modes: -- **Direct messages**: Encrypted point-to-point messages to individual contacts -- **Channel messages**: Broadcast messages to shared channels (see [Channels](channels.md)) - -This page covers direct messaging. For channel chat, see the Channels documentation. - -## How to Access - -From the Contacts screen, tap any Chat-type contact to open the ChatScreen. - -## Chat Screen Layout - -### App Bar - -- **Title**: Contact name -- **Subtitle**: Current routing path label (e.g., "2 hops", "flood (auto)", "direct (forced)") and unread count. Tapping the subtitle shows the full path details. -- **Action buttons**: - - **Routing mode** (waves icon): Switch between Auto, Direct, and Flood routing - - **Path management** (timeline icon): View recent paths with hop count, round-trip time, age, and success count. Paths are color-coded by direct repeater (green/yellow/red/blue for ranked repeaters, grey for unknown). Tap a path to activate it (the device verifies and confirms via snackbar), long-press to view full path details, set custom paths, or force flood mode. A warning banner appears when history reaches 100 entries. - - **Info** (info icon): Contact info dialog showing type, path, GPS coordinates, public key, and SMAZ compression toggle - -### Message List - -- Scrollable list with newest messages at the bottom -- **Outgoing messages**: Right-aligned, primary color background. **Failed messages** change to a red-toned error container background -- **Incoming messages**: Left-aligned, grey background with a colored avatar (initial letter or first emoji of sender name; color is deterministic from a hash of the sender name) -- Bubble width capped at 65% of screen width -- Hyperlinks rendered as tappable green underlined text -- **Pinch-to-zoom**: Two-finger zoom (0.8x–1.8x) and double-tap to reset -- **Jump to bottom**: Floating button appears when scrolled away from the bottom -- **Lazy loading**: Scrolling to top loads older messages from storage - -### Input Bar - -- **GIF button** (left): Opens GIF picker bottom sheet -- **Text field** (center): Auto-capitalization, enforces UTF-8 byte limit in real-time -- **Send button** (right): Submits the message -- On desktop: Enter/Numpad Enter also submits -- When a GIF is selected, the text field shows an inline GIF preview with a dismiss button - -## Message Types - -| Type | Wire Format | Display | -|---|---|---| -| Plain text | Raw UTF-8 string | Inline text with link detection | -| GIF | `g:` | Inline GIF image from Giphy CDN | -| Location pin | `m:,\|