diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 05c82de..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: Build - -on: - push: - branches: - - main - pull_request: - -jobs: - android: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "17" - - uses: subosito/flutter-action@v2 - with: - channel: "stable" - cache: true - - name: Cache Gradle - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties', 'android/build.gradle', 'android/settings.gradle', 'android/app/build.gradle', 'pubspec.lock') }} - restore-keys: | - ${{ runner.os }}-gradle- - - run: flutter pub get - - run: flutter build apk --release --no-pub - - ios: - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - channel: "stable" - cache: true - - run: flutter pub get - - run: flutter build ios --release --no-codesign --no-pub - - linux: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - channel: "stable" - cache: true - - name: Install Linux build deps - run: sudo apt-get update && sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev - - run: flutter pub get - - run: flutter build linux --release --no-pub - - macos: - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - channel: "stable" - cache: true - - run: flutter pub get - - run: flutter build macos --release --no-pub - - web: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - channel: "stable" - cache: true - - run: flutter pub get - - run: flutter build web --release --no-pub 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_dart.yml deleted file mode 100644 index 117eb4f..0000000 --- a/.github/workflows/flutter_dart.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Flutter and Dart - -on: - pull_request: - push: - branches: - - main - -jobs: - analyze: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - channel: stable - - - name: Install dependencies - run: flutter pub get - - - name: Analyze code - 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..97cafdb 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,13 +65,11 @@ macos/Flutter/GeneratedPluginRegistrant.swift **/ios/Flutter/Flutter.podspec # Android -.gradle/ **/android/.gradle/ **/android/captures/ **/android/local.properties **/android/.externalNativeBuild/ *.jks -key.properties keystore.properties # Generated files @@ -87,6 +80,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..7c6e251 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,5 +1,3 @@ -import java.util.Properties - plugins { id("com.android.application") id("kotlin-android") @@ -7,25 +5,19 @@ plugins { id("dev.flutter.flutter-gradle-plugin") } -val keystoreProperties = Properties() -val keystorePropertiesFile = rootProject.file("key.properties") -if (keystorePropertiesFile.exists()) { - keystorePropertiesFile.inputStream().use { keystoreProperties.load(it) } -} - 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 { @@ -48,25 +40,11 @@ android { // } } - signingConfigs { - create("release") { - val storeFilePath = keystoreProperties["storeFile"] as String? - if (storeFilePath != null) { - storeFile = file(storeFilePath) - storePassword = keystoreProperties["storePassword"] as String? - keyAlias = keystoreProperties["keyAlias"] as String? - keyPassword = keystoreProperties["keyPassword"] as String? - } - } - } - buildTypes { release { - signingConfig = if (keystorePropertiesFile.exists()) { - signingConfigs.getByName("release") - } else { - signingConfigs.getByName("debug") - } + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") } } @@ -83,5 +61,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..a4d9039 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -16,10 +16,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/assets/images/mesh-icon.png b/assets/images/mesh-icon.png deleted file mode 100644 index f7cf267..0000000 Binary files a/assets/images/mesh-icon.png and /dev/null differ 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/PRIVACY_POLICY.md b/docs/PRIVACY_POLICY.md deleted file mode 100644 index bdbe89d..0000000 --- a/docs/PRIVACY_POLICY.md +++ /dev/null @@ -1,104 +0,0 @@ -# Privacy Policy for MeshCore Open - -**Last Updated:** January 11, 2026 - -## Introduction - -MeshCore Open ("the App") is an open-source Flutter application for communicating with MeshCore LoRa mesh networking devices. This Privacy Policy explains how the App handles your information. - -## Data Collection - -### Data We Do NOT Collect - -MeshCore Open does **not**: -- Collect personal information -- Send data to external servers (except map tile requests) -- Track your usage or behavior -- Use analytics services -- Require account creation -- Share any data with third parties - -### Data Stored Locally on Your Device - -The App stores the following data **locally on your device only**: - -- **Messages**: Chat messages sent and received through the mesh network -- **Contacts**: Names and identifiers of mesh network contacts -- **App Settings**: Your preferences (theme, language, notification settings) -- **Channel Settings**: Configuration for mesh network channels -- **Message History**: Path history for message routing -- **Debug Logs**: Optional BLE and app debug logs (if enabled by user) -- **Cached Map Tiles**: Offline map data for the mapping feature - -All locally stored data remains on your device and is never transmitted to us or any third party. - -## Permissions - -The App requires certain device permissions to function: - -### Bluetooth Permissions -- **BLUETOOTH, BLUETOOTH_ADMIN** (Android 11 and below) -- **BLUETOOTH_SCAN, BLUETOOTH_CONNECT, BLUETOOTH_ADVERTISE** (Android 12+) - -These permissions are used solely to discover and communicate with MeshCore hardware devices via Bluetooth Low Energy (BLE). - -### Location Permission -- **ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION** - -Required by Android for BLE scanning on Android 11 and below. The App does not track or store your location. Location data may be optionally shared over the mesh network if you choose to enable location sharing features. - -### Internet Permission -- **INTERNET** - -Used only for downloading map tiles from OpenStreetMap tile servers when using the map feature. No personal data is transmitted. - -### Notification Permission -- **POST_NOTIFICATIONS** (Android 13+) - -Used to display notifications for incoming messages when the app is in the background. - -### Background Service Permissions -- **FOREGROUND_SERVICE, FOREGROUND_SERVICE_CONNECTED_DEVICE, WAKE_LOCK** - -Used to maintain BLE connection with your MeshCore device while the app is in the background. - -## Third-Party Services - -### Map Tiles -The App uses OpenStreetMap tile servers to display maps. When viewing maps, your device's IP address may be visible to the tile server. No other data is shared. See [OpenStreetMap's Privacy Policy](https://wiki.osmfoundation.org/wiki/Privacy_Policy) for more information. - -### GIF Search (Giphy) -The App includes a GIF picker feature powered by Giphy. When you use the GIF search feature: -- Your search queries are sent to Giphy's API servers -- Your device's IP address is visible to Giphy -- Giphy may collect usage data according to their privacy policy - -GIF search is optional and only activated when you choose to use it. See [Giphy's Privacy Policy](https://support.giphy.com/hc/en-us/articles/360032872931-GIPHY-Privacy-Policy) for more information about how they handle data. - -## Mesh Network Communications - -Messages sent through the MeshCore mesh network are transmitted over radio frequencies to other mesh devices. The App itself does not control or monitor these communications beyond facilitating the connection between your mobile device and your MeshCore hardware. - -## Data Security - -All data is stored locally on your device using standard Flutter/Android storage mechanisms. The App does not implement additional encryption for locally stored data beyond what the operating system provides. - -## Children's Privacy - -The App does not knowingly collect any personal information from children under 13 years of age. - -## Open Source - -MeshCore Open is open-source software. You can review the complete source code to verify these privacy practices at [the project repository]. - -## Changes to This Policy - -We may update this Privacy Policy from time to time. Any changes will be reflected in the "Last Updated" date at the top of this policy. - -## Contact - -If you have questions about this Privacy Policy or the App's privacy practices, please open an issue on the project's GitHub repository. - ---- - -**Summary**: MeshCore Open is a privacy-respecting app that stores all data locally on your device. We do not collect, track, or share your personal information. 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:,\|