Compare commits
387 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c78c77309e | |||
| 724696cd3c | |||
| cf6c93c50a | |||
| eee72b4c65 | |||
| 736eb064bf | |||
| 9e6968aa04 | |||
| 8904fc7208 | |||
| 02d3325d0d | |||
| 3a08f52fd4 | |||
| 3a9c14e946 | |||
| b15f6450bd | |||
| 2879bf6147 | |||
| 36f12ebded | |||
| 98f6cf3745 | |||
| 35e4d7fa6c | |||
| 0fe2502309 | |||
| a8cac76491 | |||
| 2df17be673 | |||
| 71fa8c17a4 | |||
| 698a69dc03 | |||
| 8765d0f4cc | |||
| c683dbd8f1 | |||
| b8e113d269 | |||
| e963248e37 | |||
| 68d2250afc | |||
| f4cca2fb1d | |||
| 0f3961f3a2 | |||
| 08130350d7 | |||
| c1dea86dd1 | |||
| e1b8d6ec97 | |||
| 3541b1bbac | |||
| ef6c1e0898 | |||
| eb89ad1d50 | |||
| 5ef976b3b5 | |||
| 7b4f90d646 | |||
| e1a4900dc0 | |||
| 7fc253cc88 | |||
| 445a18a83f | |||
| 8273c416b5 | |||
| b44c69f411 | |||
| 6492058a04 | |||
| 5670ab0067 | |||
| 98a9197cb1 | |||
| 55da952bfd | |||
| 9b4742951d | |||
| 43f3d439ba | |||
| ed294decfb | |||
| 5f54a2cd1b | |||
| e1536c49b1 | |||
| bfa62523df | |||
| 6813a72767 | |||
| dbd3a40bdc | |||
| 3009036565 | |||
| 80f4fd5218 | |||
| 321d4b9775 | |||
| 1c183d7e67 | |||
| a1e6f6967c | |||
| f34c2a92c3 | |||
| f1478722b0 | |||
| c867225073 | |||
| 06285a02ff | |||
| e92d215582 | |||
| d61eaaaff1 | |||
| ecde5e1109 | |||
| f548f95e29 | |||
| 9957e1fc1f | |||
| e38d03a32e | |||
| ea657a964a | |||
| 760d8e1db3 | |||
| 880df84828 | |||
| 815534d409 | |||
| becfbedc99 | |||
| 7da4e68384 | |||
| 5ea6b17b16 | |||
| 3707acb124 | |||
| 51d6210920 | |||
| 27695d372a | |||
| d06a4c1861 | |||
| 6a31d304d3 | |||
| 26fdf74d69 | |||
| b1de1b4bf0 | |||
| 3c26ce2d93 | |||
| cba1e5950c | |||
| 743ef7f124 | |||
| 13d3a107da | |||
| dfdcafb071 | |||
| c46a5ffa2f | |||
| a15f5e94da | |||
| 9d1251f21e | |||
| 0ca96ce5b1 | |||
| 33b3b04294 | |||
| b72ade1387 | |||
| 3fc038682c | |||
| 9e5b360f28 | |||
| 89360c8bc2 | |||
| 28df5a97e8 | |||
| 2e767730fd | |||
| b7fe5331d0 | |||
| 61fb39b9f8 | |||
| 9e18b99e9d | |||
| 2e4f26d7cd | |||
| 6e81fdfd46 | |||
| 5d03f99040 | |||
| 6a7dd981a2 | |||
| d68f755677 | |||
| 264d2bcc9a | |||
| e88281b7b6 | |||
| 3a11e35a7a | |||
| 8dd385beed | |||
| 2328848400 | |||
| 29e551d6cb | |||
| 531b85b8aa | |||
| 3d956596a5 | |||
| 0287de1862 | |||
| 4dd472e3c3 | |||
| bdd0d3dee2 | |||
| 11a4f9a373 | |||
| acbe4dcc49 | |||
| 9b99eadcf7 | |||
| ed0e6b6554 | |||
| 6d258154a0 | |||
| 09a83c4f22 | |||
| 5486ae8ccb | |||
| fc67d8d5aa | |||
| 225d07b440 | |||
| bac82dc9e8 | |||
| 8682e6ea67 | |||
| 30a1a36ee4 | |||
| 3fe5cdf55d | |||
| 9ada4ea601 | |||
| 945a118853 | |||
| 7a823654df | |||
| 425229fce8 | |||
| c4b3971bdd | |||
| 01d4f002f8 | |||
| 9331b02f94 | |||
| 27f7932db9 | |||
| 4a39a7cd46 | |||
| a50c0d0b2d | |||
| 3f1bd54889 | |||
| 91759f531a | |||
| 87708e2684 | |||
| 73e5b80232 | |||
| 28a5913ba3 | |||
| 091c754584 | |||
| cf27c59c84 | |||
| 119590434a | |||
| dc82414447 | |||
| 23f29f2cda | |||
| d66b16a4e8 | |||
| d6647f4701 | |||
| 72448f67d0 | |||
| bc5f299350 | |||
| 6d97ad6855 | |||
| 87b0fd6fc7 | |||
| 1fbe1823cb | |||
| 4a066e08dc | |||
| f941f0dbfa | |||
| 94ed729985 | |||
| 8296f44a4e | |||
| 4e70fd2a5e | |||
| ca02197414 | |||
| 352a6c427e | |||
| 5f9259e41f | |||
| 75ae903b99 | |||
| 8892823337 | |||
| e738664f89 | |||
| e37616fa15 | |||
| 6e2abfeb2d | |||
| b62c1c7338 | |||
| 17379394bd | |||
| 2763d83fe4 | |||
| 77018dc358 | |||
| 1f6b2dacf9 | |||
| 21c58d4e13 | |||
| 3af97ff6dd | |||
| 703d5a1ec4 | |||
| d2a6fbe182 | |||
| e801a497f8 | |||
| e92a66ff28 | |||
| 6900e5c3db | |||
| 966a8d0d2c | |||
| 3ec3b05fb8 | |||
| 14a93e9bf5 | |||
| c229b0369e | |||
| 9f332e93be | |||
| b472ea8c70 | |||
| a67c6d81c3 | |||
| 91ae4dab90 | |||
| 08ac60a408 | |||
| d4da34fcf7 | |||
| 74840d3baf | |||
| 4a72fbd1ad | |||
| dbe0a5411b | |||
| dc3325ec46 | |||
| a92e57bb64 | |||
| e21f3106d0 | |||
| 0dcb5f05f0 | |||
| f501d11ec6 | |||
| dfcf13a97b | |||
| ccd23c4b81 | |||
| 00636c9084 | |||
| accec1681b | |||
| 67238468ce | |||
| bc5b12f1ef | |||
| c09af98bef | |||
| ae32e76563 | |||
| 5572c9ee75 | |||
| f6cc000788 | |||
| 75b0d198bc | |||
| 1947cd9f3e | |||
| f1d93bd5e8 | |||
| f63d50f0da | |||
| eb597b6c68 | |||
| efe21c4e87 | |||
| 38fece3313 | |||
| 3af3cce606 | |||
| 026ec6f7de | |||
| eb50249b93 | |||
| ca6058eccd | |||
| 99c0ab7e22 | |||
| 2950a9a687 | |||
| 1b3de54873 | |||
| 20a9ef3c2b | |||
| a741e12ad1 | |||
| e54f30d6fb | |||
| e1d23ad2c7 | |||
| f07993b367 | |||
| 0e5f1a45c4 | |||
| f10aeaeba8 | |||
| 00e4f52d75 | |||
| 3ea2e4763e | |||
| 94d9afe8b1 | |||
| 7db3a12723 | |||
| fcf10b4a73 | |||
| 7f353490cf | |||
| 46683e0ec2 | |||
| 4e368d562d | |||
| 38f6e42796 | |||
| f56c28a27d | |||
| 92d3009eb4 | |||
| 7a4ac9ae9b | |||
| f8d00caae0 | |||
| e03d80b71f | |||
| b7d0db8d1c | |||
| 6ae3f612ae | |||
| 40d3941aab | |||
| e53c493e78 | |||
| 54e0dae172 | |||
| 066aba7c5d | |||
| 5e446207c6 | |||
| 609d0c8dbc | |||
| 820bac0db0 | |||
| d3c7d8e43a | |||
| 0c1e163b88 | |||
| d0d6a34fb5 | |||
| 16ce1359d7 | |||
| 9fe4a3710d | |||
| 8611adab1f | |||
| 7d457cb863 | |||
| 297516fc80 | |||
| 6b6a881c7a | |||
| 8ef8a38495 | |||
| ddcda4ba5a | |||
| 5cfe45b953 | |||
| b572314ae9 | |||
| e97fb9bd24 | |||
| 1c9c089a53 | |||
| cb3b5a84eb | |||
| a4bbeffddc | |||
| 37ec8f2f05 | |||
| 39cd6d5514 | |||
| 44eb4fad58 | |||
| 1a209cbcfc | |||
| 33a8f34463 | |||
| ce8e8f0d5b | |||
| aa2d0f1927 | |||
| 0e074fd806 | |||
| 0757c8e53a | |||
| add4731d05 | |||
| 7dc162d968 | |||
| 8ba4bbfbc5 | |||
| cac6abfef1 | |||
| 5354acb1d3 | |||
| fae416fb34 | |||
| 69433b6d89 | |||
| ea3b9609fc | |||
| 20a9939314 | |||
| c7b7deb0f6 | |||
| 82e04e8090 | |||
| f299608296 | |||
| 7dcec5b4ee | |||
| e4684b585a | |||
| 8386f262e1 | |||
| 45cd8a56a3 | |||
| 754f8a6c62 | |||
| c4f54efd77 | |||
| 637e08d22c | |||
| 32dc0fca22 | |||
| b5aa294fc1 | |||
| 26516baf67 | |||
| 4879b136f8 | |||
| bdd7fc0cdd | |||
| 5ea044af10 | |||
| 9d20be1c06 | |||
| 9436c2d45a | |||
| 17e55e96bb | |||
| e4cfbb57b4 | |||
| d9f9ff58b4 | |||
| a059f1be45 | |||
| 9e46f8b44c | |||
| a934781009 | |||
| 5fe6738f25 | |||
| c1bcf261d7 | |||
| b570539a2d | |||
| 89a14c2719 | |||
| 4ad01ed43c | |||
| ffaa4033ae | |||
| 1a4fd1b477 | |||
| e1555ce380 | |||
| c7933d363b | |||
| 08ffb978cf | |||
| c5ec60638c | |||
| 75ec3b6116 | |||
| 45c9823c6f | |||
| 45658a7612 | |||
| a14833494e | |||
| 457b44de3a | |||
| 36d4a10396 | |||
| 77566b0fe1 | |||
| 7633327f45 | |||
| 6b4b2d7ce6 | |||
| 10b63e0df2 | |||
| ba6d751346 | |||
| 96d222a580 | |||
| 01ad8471cc | |||
| 2b826757cb | |||
| 9bf649e2c6 | |||
| c7a2bf9a95 | |||
| 82adbd761b | |||
| 9a8bdf00dc | |||
| 8b30342113 | |||
| 817c60a155 | |||
| f08e86cf97 | |||
| a6bb9490a1 | |||
| e4e8bfa4ef | |||
| d1e45fc2ba | |||
| 32fa96431e | |||
| 1e9508d401 | |||
| 36697c6e61 | |||
| c9145c99d3 | |||
| 6b6d9caeeb | |||
| d0e3767db6 | |||
| f9cb0c80a5 | |||
| a26d14bd46 | |||
| 411cd3f8d2 | |||
| 38f4de80b6 | |||
| 7de07c023f | |||
| c272c60f9a | |||
| eca78453d6 | |||
| 3754cf14ea | |||
| 834850fb51 | |||
| e7e2bb91b8 | |||
| 4c492f69ef | |||
| 50f2a8b439 | |||
| ebbc367fec | |||
| 14f3429eb5 | |||
| e49e80d330 | |||
| d07372c7e0 | |||
| 990f2bd33d | |||
| 29660d520e | |||
| 0ef2194fb0 | |||
| 3664ae34cd | |||
| 1b94442ab6 | |||
| 3ae14781f0 | |||
| ecc496f2af | |||
| 87b25655d0 | |||
| c47a4cb622 | |||
| a30fc439f3 | |||
| afcc4db405 | |||
| 87bcb6a6a3 | |||
| 68bb031bb6 | |||
| c4f5c7b171 | |||
| 2bce14224d | |||
| fd305fd55b | |||
| d0dd805244 | |||
| 8668564464 |
@@ -0,0 +1,3 @@
|
|||||||
|
github: [zjs81]
|
||||||
|
custom:
|
||||||
|
- "https://paypal.me/zjs82"
|
||||||
@@ -33,6 +33,9 @@ migrate_working_dir/
|
|||||||
pubspec.lock
|
pubspec.lock
|
||||||
/build/
|
/build/
|
||||||
/coverage/
|
/coverage/
|
||||||
|
# fvm project files
|
||||||
|
.fvm/
|
||||||
|
.fvmrc
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
@@ -84,6 +87,10 @@ keystore.properties
|
|||||||
# IDE
|
# IDE
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
|
.contextstream/
|
||||||
|
|
||||||
# Cloudflare Wrangler
|
# Cloudflare Wrangler
|
||||||
.wrangler
|
.wrangler
|
||||||
|
|
||||||
|
# Claude Code local working dir (worktrees, jobs, settings)
|
||||||
|
.claude/
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## BLE Frames & Protocol Notes
|
## 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`.
|
- 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 name prefix `MeshCore-` and filters by `platformName`/`advertisementData.advName`.
|
- Discovery: scans for device names matching known prefixes 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`.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# MeshCore Open - Flutter Client
|
# MeshCore Open - Flutter Client
|
||||||
|
|
||||||
Open-source Flutter client for MeshCore LoRa mesh networking devices.
|
Open-source Flutter client for MeshCore LoRa mesh networking devices. Connects to MeshCore-compatible radios over **BLE, TCP, or USB serial** and provides direct/channel chat, contact and channel management, on-map node tracking, repeater administration, and on-device message translation.
|
||||||
|
|
||||||
## Build Commands
|
## Build Commands
|
||||||
|
|
||||||
@@ -17,6 +17,9 @@ Open-source Flutter client for MeshCore LoRa mesh networking devices.
|
|||||||
# Build iOS
|
# Build iOS
|
||||||
~/flutter/bin/flutter build ios
|
~/flutter/bin/flutter build ios
|
||||||
|
|
||||||
|
# Build versioned web release (uses build_pipe)
|
||||||
|
~/flutter/bin/dart run build_pipe
|
||||||
|
|
||||||
# Run static analysis
|
# Run static analysis
|
||||||
~/flutter/bin/flutter analyze
|
~/flutter/bin/flutter analyze
|
||||||
|
|
||||||
@@ -28,43 +31,132 @@ Open-source Flutter client for MeshCore LoRa mesh networking devices.
|
|||||||
|
|
||||||
```
|
```
|
||||||
lib/
|
lib/
|
||||||
├── main.dart # App entry point, MaterialApp setup with Provider
|
├── main.dart # Entry point: MultiProvider wiring, locale + theme, initial route
|
||||||
├── connector/
|
├── connector/ # Unified BLE/TCP/USB transport layer
|
||||||
│ └── meshcore_connector.dart # BLE communication layer (MeshCoreConnector)
|
│ ├── meshcore_connector.dart # Central state holder + ChangeNotifier (all transports)
|
||||||
├── screens/
|
│ ├── meshcore_connector_tcp.dart # TCP transport helper
|
||||||
│ ├── scanner_screen.dart # BLE device scanning (home screen)
|
│ ├── meshcore_connector_usb.dart # USB serial transport helper
|
||||||
│ ├── device_screen.dart # Connected device hub with navigation
|
│ ├── meshcore_protocol.dart # Frame size + version constants
|
||||||
│ ├── chat_screen.dart # Chat interface (placeholder)
|
│ └── meshcore_uuids.dart # Nordic UART UUIDs + scan name prefixes
|
||||||
│ ├── contacts_screen.dart # Contacts list (placeholder)
|
├── models/ # Plain data classes (Contact, Channel, Message, Community, …)
|
||||||
│ └── settings_screen.dart # Device info and app settings
|
├── services/ # ChangeNotifier services + IO services (retry, translation, ML, …)
|
||||||
└── widgets/
|
├── storage/ # SharedPreferences-backed stores, scoped per device key
|
||||||
└── device_tile.dart # Device list item with signal strength
|
├── helpers/ # Pure utilities (Smaz compression, GIF parsing, scroll helpers, path hop resolution)
|
||||||
|
├── utils/ # Platform / IO / UX utilities (logger, GPX export, dialogs)
|
||||||
|
├── theme/ # MeshPalette (defined, not yet wired in main.dart)
|
||||||
|
├── l10n/ # ARB localization for 18 locales
|
||||||
|
├── icons/ # Custom icon widgets
|
||||||
|
├── widgets/ # Reusable widgets (AppBar, BatteryUi, QR, jump-to-bottom, …)
|
||||||
|
└── screens/ # ~26 screens — see Screens section below
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Screens
|
||||||
|
|
||||||
|
All screens are fully implemented (no remaining placeholders).
|
||||||
|
|
||||||
|
### Connection / Scanning
|
||||||
|
| Screen | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `scanner_screen.dart` | BLE device scan and connect — main entry point |
|
||||||
|
| `tcp_screen.dart` | Connect to a MeshCore device over TCP/IP |
|
||||||
|
| `usb_screen.dart` | Connect to a MeshCore device over USB serial |
|
||||||
|
| `discovery_screen.dart` | Browse all discovered (non-contact) mesh nodes |
|
||||||
|
| `chrome_required_screen.dart` | Web gate for non-Chrome browsers (BLE unavailable) |
|
||||||
|
|
||||||
|
### Chat / Messaging
|
||||||
|
| Screen | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `chat_screen.dart` | Direct (private) messaging with a contact |
|
||||||
|
| `channel_chat_screen.dart` | Group messaging inside a named channel |
|
||||||
|
| `channels_screen.dart` | List and manage channels (add/edit/delete) |
|
||||||
|
| `channel_message_path_screen.dart` | Hop-by-hop route a channel message took, with map overlay |
|
||||||
|
|
||||||
|
### Contacts / Neighbors
|
||||||
|
| Screen | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `contacts_screen.dart` | Full contacts list with previews and management |
|
||||||
|
| `neighbors_screen.dart` | Nodes directly heard by the connected radio (one-hop) |
|
||||||
|
|
||||||
|
### Repeater Management
|
||||||
|
| Screen | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `repeater_hub_screen.dart` | Top-level repeater hub; navigates to sub-screens |
|
||||||
|
| `repeater_status_screen.dart` | Live status of a managed repeater node |
|
||||||
|
| `repeater_cli_screen.dart` | Raw command-line interface to a repeater |
|
||||||
|
| `repeater_settings_screen.dart` | Full radio/node settings editor for a repeater |
|
||||||
|
|
||||||
|
### Map / Location
|
||||||
|
| Screen | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `map_screen.dart` | Main map view of contacts/nodes with live GPS positions |
|
||||||
|
| `line_of_sight_map_screen.dart` | Terrain LOS analysis between configurable endpoints |
|
||||||
|
| `path_trace_map.dart` | Animates the hop path a direct message traveled |
|
||||||
|
| `map_cache_screen.dart` | Download/clear offline map tile cache |
|
||||||
|
| `community_qr_scanner_screen.dart` | Scan QR to join a mesh community/channel |
|
||||||
|
|
||||||
|
### Settings / Debug / Diagnostics
|
||||||
|
| Screen | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `settings_screen.dart` | Connected device settings: radio params, identity, GPS |
|
||||||
|
| `app_settings_screen.dart` | App preferences: theme, units, map source, notifications |
|
||||||
|
| `app_debug_log_screen.dart` | In-app log viewer (app-layer messages) |
|
||||||
|
| `ble_debug_log_screen.dart` | In-app log viewer (raw BLE frame traffic) |
|
||||||
|
| `companion_radio_stats_screen.dart` | RF stats (RSSI, SNR, packet counts) for paired radio |
|
||||||
|
| `telemetry_screen.dart` | Battery / sensor / environmental telemetry for a contact |
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### State Management
|
### State Management
|
||||||
- **Provider** with `ChangeNotifier` pattern
|
|
||||||
- `MeshCoreConnector` is the central state holder for BLE connection
|
`Provider` with `ChangeNotifier`. `main.dart` wires a `MultiProvider` with the following:
|
||||||
- Screens use `Consumer<MeshCoreConnector>` for reactive UI updates
|
|
||||||
|
| Provider | Role |
|
||||||
|
|---|---|
|
||||||
|
| `MeshCoreConnector` | Active transport (BLE/TCP/USB), connection state, frame I/O |
|
||||||
|
| `MessageRetryService` | ACK tracking and retry scheduling with backoff |
|
||||||
|
| `PathHistoryService` | Per-contact routing history (LRU cache, 50 contacts) |
|
||||||
|
| `AppSettingsService` | App preferences (theme, units, locale, notifications) |
|
||||||
|
| `BleDebugLogService` | Raw BLE frame log buffer |
|
||||||
|
| `AppDebugLogService` | Structured app log buffer |
|
||||||
|
| `ChatTextScaleService` | Pinch-to-zoom text scale for chat screens |
|
||||||
|
| `TranslationService` | On-device LLM translation (llamadart) |
|
||||||
|
| `UiViewStateService` | Contacts/channels sort/filter/search state |
|
||||||
|
| `TimeoutPredictionService` | ML linear regression for ACK timeout prediction |
|
||||||
|
| `StorageService` | Path history + delivery observation persistence |
|
||||||
|
| `MapTileCacheService` | OSM tile pre-cache |
|
||||||
|
|
||||||
|
Screens consume these via `Consumer<T>` (or `context.watch<T>()` / `context.read<T>()`) for reactive UI.
|
||||||
|
|
||||||
|
### Storage / Persistence
|
||||||
|
|
||||||
|
All stores in `lib/storage/` use `PrefsManager` (a `SharedPreferences` singleton initialized in `main()`). Most stores **scope keys by the first 10 hex chars of the connected device's public key**, so per-radio data is isolated.
|
||||||
|
|
||||||
|
| Store | Persists |
|
||||||
|
|---|---|
|
||||||
|
| `message_store`, `channel_message_store` | Direct + channel messages |
|
||||||
|
| `contact_store`, `contact_discovery_store` | Known + discovered contacts |
|
||||||
|
| `channel_store`, `channel_order_store`, `channel_settings_store` | Channels, display order, per-channel Smaz toggle |
|
||||||
|
| `community_store` | Communities (32-byte shared secrets) |
|
||||||
|
| `contact_group_store`, `contact_settings_store` | Groups, per-contact Smaz toggle |
|
||||||
|
| `unread_store` | Per-contact unread counts (debounced writes) |
|
||||||
|
|
||||||
|
GGUF translation models are stored as files (not SharedPreferences) via `translation_file_store`.
|
||||||
|
|
||||||
### Theming
|
### Theming
|
||||||
- Material 3 design (`useMaterial3: true`)
|
- Material 3 design (`useMaterial3: true`)
|
||||||
- System-based dark/light mode (`ThemeMode.system`)
|
- System-based dark/light mode (`ThemeMode.system`)
|
||||||
- Blue color scheme seed
|
- Blue color scheme seed
|
||||||
|
- `lib/theme/mesh_theme.dart` defines a warm-dark `MeshPalette` (phosphor-green accents) but is **not currently wired** in `main.dart` — available for a future redesign
|
||||||
|
|
||||||
## BLE Protocol
|
### Localization
|
||||||
|
|
||||||
### Nordic UART Service (NUS)
|
18 locales supported via Flutter's standard ARB pipeline (`lib/l10n/`): en, de, es, fr, it, pt, ru, uk, bg, hu, ja, ko, nl, pl, sk, sl, sv, zh. Language override comes from `AppSettingsService.settings.languageOverride`. Use the `context.l10n` extension (`lib/l10n/l10n.dart`) for translated strings; contact-type names live in `contact_localization.dart`.
|
||||||
- **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
|
## Transports
|
||||||
- Scans for devices with name prefix `MeshCore-`
|
|
||||||
- Filters by `platformName` or `advertisementData.advName`
|
|
||||||
|
|
||||||
### Connection States
|
`MeshCoreConnector` unifies all three transports under one `ChangeNotifier`. There is **no shared base class** — selection is via the `MeshCoreTransportType { bluetooth, usb, tcp }` enum, and BLE/TCP/USB share the same connection-state enum, send/receive API, and frame protocol.
|
||||||
|
|
||||||
|
### Connection State
|
||||||
```dart
|
```dart
|
||||||
enum MeshCoreConnectionState {
|
enum MeshCoreConnectionState {
|
||||||
disconnected,
|
disconnected,
|
||||||
@@ -75,28 +167,137 @@ enum MeshCoreConnectionState {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Frame I/O
|
### Frame I/O (all transports)
|
||||||
- **Send**: `MeshCoreConnector.sendFrame(Uint8List data)`
|
- **Send**: `MeshCoreConnector.sendFrame(Uint8List data, {String? channelSendQueueId, bool expectsGenericAck})`
|
||||||
- **Receive**: `MeshCoreConnector.receivedFrames` stream of `Uint8List`
|
- **Receive**: `Stream<Uint8List> get receivedFrames`
|
||||||
|
- **Protocol constants** (`meshcore_protocol.dart`): `maxFrameSize = 172`, `maxTextPayloadBytes = 160`, `appProtocolVersion = 4`
|
||||||
|
|
||||||
|
### BLE — Nordic UART Service (NUS)
|
||||||
|
- **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`
|
||||||
|
- **Discovery**: scans for devices whose name starts with `MeshCore-`, `Whisper-`, `WisCore-`, `Seeed`, `Lilygo`, `HT-`, or `LowMesh_MC_` (filters on both `platformName` and `advertisementData.advName`)
|
||||||
|
- **Linux**: `linux_ble_pairing_service.dart` falls back to `bluetoothctl` when BlueZ agent prompts fail
|
||||||
|
|
||||||
|
### TCP
|
||||||
|
- Manual host/port entry, persisted via `AppSettingsService` (`tcpServerAddress`, `tcpServerPort`)
|
||||||
|
- UI hint: `192.168.40.10` / port `5000`
|
||||||
|
- Disabled on web (`PlatformInfo.isWeb`)
|
||||||
|
- API: `MeshCoreConnector.connectTcp(host: ..., port: ...)`
|
||||||
|
|
||||||
|
### USB Serial (flserial)
|
||||||
|
- Default baud rate: `115200`
|
||||||
|
- Port enumeration: `MeshCoreConnector.listUsbPorts()`
|
||||||
|
- COBS-framed packets via `usb_serial_frame_codec.dart`
|
||||||
|
- macOS device-name resolution via `ioreg` (`utils/macos_usb_device_names.dart`)
|
||||||
|
- API: `MeshCoreConnector.connectUsb(portName: ..., baudRate: 115200)`
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
App version: `9.5.0+13` — Dart SDK constraint: `^3.9.2`
|
||||||
|
|
||||||
|
**Connectivity**
|
||||||
|
|
||||||
| Package | Version | Purpose |
|
| Package | Version | Purpose |
|
||||||
|---------|---------|---------|
|
|---------|---------|---------|
|
||||||
| flutter_blue_plus | ^2.1.0 | BLE communication |
|
| flutter_blue_plus | ^2.1.0 | BLE scanning, connecting, and UART data transfer |
|
||||||
| provider | ^6.1.5+1 | State management |
|
| flutter_blue_plus_platform_interface | ^9.0.2 | Platform-interface layer required by flutter_blue_plus |
|
||||||
| cupertino_icons | ^1.0.8 | iOS-style icons |
|
| flserial | git (MeshEnvy fork) | USB serial transport for wired device connections (TODO: upstream pending) |
|
||||||
|
|
||||||
|
**State / Storage**
|
||||||
|
|
||||||
|
| Package | Version | Purpose |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| provider | ^6.1.5+1 | ChangeNotifier-based state management across screens |
|
||||||
|
| shared_preferences | ^2.2.2 | Persistent key-value storage for user settings |
|
||||||
|
| path_provider | ^2.1.5 | Locates platform-appropriate directories for file I/O |
|
||||||
|
|
||||||
|
**Crypto**
|
||||||
|
|
||||||
|
| Package | Version | Purpose |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| crypto | ^3.0.3 | SHA/HMAC hashing used in message authentication |
|
||||||
|
| pointycastle | ^4.0.0 | AES encryption/decryption for channel and direct messages |
|
||||||
|
| uuid | ^4.3.3 | Generates UUIDs for message and contact identity |
|
||||||
|
|
||||||
|
**Maps & Location**
|
||||||
|
|
||||||
|
| Package | Version | Purpose |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| flutter_map | ^8.2.2 | Interactive tile map for node positions and path traces |
|
||||||
|
| latlong2 | ^0.9.1 | LatLng coordinate type used throughout map and GPS code |
|
||||||
|
| gpx | ^2.3.0 | Export node paths as GPX track files |
|
||||||
|
|
||||||
|
**UI**
|
||||||
|
|
||||||
|
| Package | Version | Purpose |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| material_symbols_icons | ^4.2928.1 | Extended Material Symbols icon set (line-of-sight, etc.) |
|
||||||
|
| flutter_svg | ^2.0.10+1 | Renders SVG assets (custom icons such as LoS indicator) |
|
||||||
|
| cached_network_image | ^3.4.1 | Caches map tile images downloaded over the network |
|
||||||
|
| flutter_cache_manager | ^3.4.1 | Underlying cache manager used by cached_network_image |
|
||||||
|
| flutter_linkify | ^6.0.0 | Auto-detects and makes URLs tappable in chat messages |
|
||||||
|
| mobile_scanner | ^7.1.4 | QR/barcode scanning for contact and channel import |
|
||||||
|
| qr_flutter | ^4.1.0 | Generates QR codes for sharing contacts and channels |
|
||||||
|
| cupertino_icons | ^1.0.8 | iOS-style icon font (bundled for completeness) |
|
||||||
|
| characters | ^1.4.0 | Unicode-aware string operations for message text handling |
|
||||||
|
|
||||||
|
**Notifications / Background**
|
||||||
|
|
||||||
|
| Package | Version | Purpose |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| flutter_local_notifications | ^22.0.0 | Shows local push notifications for incoming messages |
|
||||||
|
| flutter_foreground_task | ^9.2.0 | Keeps the app alive in background to maintain BLE/USB connection |
|
||||||
|
|
||||||
|
**ML / AI**
|
||||||
|
|
||||||
|
| Package | Version | Purpose |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| ml_algo | ^16.0.0 | OLS regression used in `timeout_prediction_service.dart` to predict message ACK timeouts |
|
||||||
|
| ml_dataframe | ^1.0.0 | DataFrame input format required by ml_algo |
|
||||||
|
| llamadart | ^0.8.0 | On-device LLM inference used in `translation_service.dart` for message translation |
|
||||||
|
| flutter_langdetect | ^0.0.1 | Detects a message's source language in `translation_service.dart` before translating |
|
||||||
|
|
||||||
|
**Misc**
|
||||||
|
|
||||||
|
| Package | Version | Purpose |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| http | ^1.2.0 | Fetches tile URLs and any remote API calls |
|
||||||
|
| url_launcher | ^6.3.0 | Opens URLs in the system browser from linkified chat text |
|
||||||
|
| share_plus | ^13.1.0 | Shares files (e.g. exported GPX tracks) via the system share sheet |
|
||||||
|
| package_info_plus | ^10.1.0 | Reads app version/build number displayed in settings |
|
||||||
|
| web | ^1.1.1 | Web-platform APIs for USB serial and browser detection on Flutter Web |
|
||||||
|
| intl | any | Internationalization and locale formatting (required by flutter_localizations) |
|
||||||
|
| build_pipe | ^0.3.1 | CI/CD build pipeline configuration (web release builds with versioned assets) |
|
||||||
|
|
||||||
## Platform Configuration
|
## Platform Configuration
|
||||||
|
|
||||||
### Android (`android/app/src/main/AndroidManifest.xml`)
|
### Android (`android/app/src/main/AndroidManifest.xml`)
|
||||||
- `BLUETOOTH`, `BLUETOOTH_ADMIN` (API 30 and below)
|
- `INTERNET` (map tiles, translation model downloads)
|
||||||
- `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, `BLUETOOTH_ADVERTISE` (API 31+)
|
- `BLUETOOTH`, `BLUETOOTH_ADMIN` (API ≤ 30)
|
||||||
- `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION` (for BLE scanning)
|
- `BLUETOOTH_SCAN` (with `neverForLocation`), `BLUETOOTH_CONNECT`, `BLUETOOTH_ADVERTISE` (API 31+)
|
||||||
|
- `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION` (BLE scanning on API ≤ 30)
|
||||||
|
- `POST_NOTIFICATIONS` (API 33+)
|
||||||
|
- `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_CONNECTED_DEVICE` (background BLE/USB connection)
|
||||||
|
- `WAKE_LOCK`
|
||||||
|
- `CAMERA` (QR scanning, declared as optional feature)
|
||||||
|
- USB host hardware feature (optional)
|
||||||
|
|
||||||
|
`flutter_foreground_task` registers a `ForegroundService` with `foregroundServiceType="connectedDevice"` and `stopWithTask="false"`.
|
||||||
|
|
||||||
|
**Build config (`android/app/build.gradle.kts`)**: `applicationId = com.meshcore.meshcore_open`, NDK `29.0.14206865`, Java 8 core-library desugaring (`desugar_jdk_libs:2.1.4`), release signing via `key.properties` (debug fallback).
|
||||||
|
|
||||||
### iOS (`ios/Runner/Info.plist`)
|
### iOS (`ios/Runner/Info.plist`)
|
||||||
- `NSBluetoothAlwaysUsageDescription`
|
- `NSBluetoothAlwaysUsageDescription`, `NSBluetoothPeripheralUsageDescription`
|
||||||
- `NSBluetoothPeripheralUsageDescription`
|
- `NSCameraUsageDescription` (QR scanning to join communities)
|
||||||
|
- Background modes: `bluetooth-central`
|
||||||
|
- `LSApplicationQueriesSchemes`: `http`, `https`
|
||||||
|
|
||||||
|
### Web (`web/`)
|
||||||
|
PWA scaffold present but boilerplate (`manifest.json` and `index.html` are unmodified Flutter defaults). BLE is unsupported in browsers; TCP and Web Serial USB may work in Chrome only. `ChromeRequiredScreen` gates non-Chrome web users. Versioned releases are produced via `build_pipe` (`?v=<pubspec version>` cache busting, no service worker).
|
||||||
|
|
||||||
|
### Desktop
|
||||||
|
`linux/`, `windows/`, and `macos/` directories are present as Flutter scaffolds. No app-specific native config has been added; BLE on desktop has not been validated.
|
||||||
|
|
||||||
## Coding Conventions
|
## Coding Conventions
|
||||||
|
|
||||||
@@ -123,14 +324,14 @@ enum MeshCoreConnectionState {
|
|||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `lib/connector/meshcore_connector.dart` | All BLE logic - scanning, connecting, data transfer |
|
| `lib/main.dart` | App configuration, MultiProvider setup, theme, locale, initial route |
|
||||||
| `lib/screens/scanner_screen.dart` | Entry point UI, device list |
|
| `lib/connector/meshcore_connector.dart` | Unified BLE/TCP/USB transport state holder |
|
||||||
| `lib/main.dart` | App configuration, theme, Provider setup |
|
| `lib/connector/meshcore_protocol.dart` | Frame size limits and protocol version |
|
||||||
| `pubspec.yaml` | Dependencies and project metadata |
|
| `lib/connector/meshcore_uuids.dart` | NUS UUIDs and BLE scan name prefixes |
|
||||||
|
| `lib/services/app_settings_service.dart` | App-wide settings (`AppSettings` JSON in SharedPreferences) |
|
||||||
## Placeholder Screens
|
| `lib/services/storage_service.dart` | Path history + delivery observation persistence |
|
||||||
|
| `lib/services/message_retry_service.dart` | ACK tracking + retry scheduling |
|
||||||
The following screens are implemented as placeholders and need full implementation:
|
| `lib/services/translation_service.dart` | On-device LLM translation (llamadart) |
|
||||||
- `chat_screen.dart` - Mesh chat functionality
|
| `lib/storage/prefs_manager.dart` | SharedPreferences singleton initialized in `main()` |
|
||||||
- `contacts_screen.dart` - Contact management
|
| `lib/screens/scanner_screen.dart` | Home screen — BLE scan and connect |
|
||||||
- `settings_screen.dart` - Radio settings, node identity, location (partially implemented)
|
| `pubspec.yaml` | Dependencies and project metadata (current version `9.5.0+13`) |
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# 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.
|
||||||
@@ -6,6 +6,8 @@ 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.
|
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.
|
||||||
|
|
||||||
|
**Website:** [meshcoreopen.org](https://meshcoreopen.org/)
|
||||||
|
|
||||||
<a href="http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/zjs81/meshcore-open">
|
<a href="http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/zjs81/meshcore-open">
|
||||||
<img src="assets/badges/badge_obtainium.png" height="80" align="center" alt="Get it on Obtainium"/>
|
<img src="assets/badges/badge_obtainium.png" height="80" align="center" alt="Get it on Obtainium"/>
|
||||||
</a>
|
</a>
|
||||||
@@ -46,7 +48,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
|||||||
- **Live Map View**: Real-time visualization of mesh network nodes on an interactive map
|
- **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
|
- **Node Filtering**: Filter by node type (chat, repeater, sensor) and time range
|
||||||
- **Location Sharing**: Share GPS coordinates and custom markers with contacts
|
- **Location Sharing**: Share GPS coordinates and custom markers with contacts
|
||||||
- **Offline Maps**: Download map tiles for offline use in remote areas
|
- **Offline Maps**: Download map tiles for offline use in remote areas (with [StadiaMaps](https://stadiamaps.com/pricing/) Free Subscription API-Key)
|
||||||
- **MGRS Coordinates**: Support for Military Grid Reference System coordinate format
|
- **MGRS Coordinates**: Support for Military Grid Reference System coordinate format
|
||||||
|
|
||||||
### Device Management
|
### Device Management
|
||||||
@@ -92,12 +94,12 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
|||||||
|---------|---------|
|
|---------|---------|
|
||||||
| flutter_blue_plus | Bluetooth Low Energy communication |
|
| flutter_blue_plus | Bluetooth Low Energy communication |
|
||||||
| provider | State management |
|
| provider | State management |
|
||||||
| sqflite | Local database storage |
|
| shared_preferences | Local key-value storage (scoped per device) |
|
||||||
| flutter_map | Interactive map display |
|
| flutter_map | Interactive map display |
|
||||||
| latlong2 | Geographic coordinate handling |
|
| latlong2 | Geographic coordinate handling |
|
||||||
| flutter_local_notifications | Background notification support |
|
| flutter_local_notifications | Background notification support |
|
||||||
| smaz | Message compression |
|
|
||||||
| pointycastle | Cryptographic operations |
|
| pointycastle | Cryptographic operations |
|
||||||
|
| llamadart | On-device LLM message translation |
|
||||||
| intl | Internationalization and date formatting |
|
| intl | Internationalization and date formatting |
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
@@ -150,7 +152,8 @@ lib/
|
|||||||
├── main.dart # App entry point
|
├── main.dart # App entry point
|
||||||
├── connector/
|
├── connector/
|
||||||
│ ├── meshcore_connector.dart # BLE communication & state management
|
│ ├── meshcore_connector.dart # BLE communication & state management
|
||||||
│ └── meshcore_protocol.dart # Protocol definitions & frame parsing
|
│ ├── meshcore_protocol.dart # Protocol definitions & frame parsing
|
||||||
|
│ └── meshcore_uuids.dart # Device names and IDs (add prefixes here!)
|
||||||
├── screens/
|
├── screens/
|
||||||
│ ├── scanner_screen.dart # Device scanning (home screen)
|
│ ├── scanner_screen.dart # Device scanning (home screen)
|
||||||
│ ├── contacts_screen.dart # Contact list
|
│ ├── contacts_screen.dart # Contact list
|
||||||
@@ -184,7 +187,16 @@ lib/
|
|||||||
|
|
||||||
### Device Discovery
|
### Device Discovery
|
||||||
|
|
||||||
Devices are discovered by scanning for BLE advertisements with the name prefix `MeshCore-`
|
Devices are discovered by scanning for BLE advertisements with known MeshCore device name prefixes. These are currently:
|
||||||
|
- `MeshCore-`
|
||||||
|
- `Whisper-`
|
||||||
|
- `WisCore-`
|
||||||
|
- `HT-`
|
||||||
|
- `LowMesh_MC_`
|
||||||
|
- `NRF52`
|
||||||
|
|
||||||
|
New device prefixes can be added in `lib/connector/meshcore_uuids.dart`.
|
||||||
|
|
||||||
|
|
||||||
### Message Format
|
### Message Format
|
||||||
|
|
||||||
@@ -212,6 +224,10 @@ Messages are transmitted as binary frames using a custom protocol optimized for
|
|||||||
|
|
||||||
This is an open-source project. Contributions are welcome!
|
This is an open-source project. Contributions are welcome!
|
||||||
|
|
||||||
|
## SWHID and Archive badge
|
||||||
|
[](https://archive.softwareheritage.org/browse/origin/?origin_url=https://github.com/zjs81/meshcore-open)
|
||||||
|
[](https://archive.softwareheritage.org/swh:1:dir:d37a80b06359730864150ad2aeadd46cce9abd55;origin=https://github.com/zjs81/meshcore-open;visit=swh:1:snp:47656c4b55ab40a689ff8d2f045196725f05096b;anchor=swh:1:rev:0fe250230905fdd05dbedc0f546736990beacf53)
|
||||||
|
|
||||||
### Development Guidelines
|
### Development Guidelines
|
||||||
|
|
||||||
- Follow the Flutter style guide
|
- Follow the Flutter style guide
|
||||||
|
|||||||
@@ -43,9 +43,22 @@ android {
|
|||||||
// arguments += listOf("-DANDROID_STL=c++_shared")
|
// arguments += listOf("-DANDROID_STL=c++_shared")
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// ndk {
|
// arm64-v8a only, deliberately.
|
||||||
// abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86_64")
|
//
|
||||||
// }
|
// * ONNX Runtime (flutter_onnxruntime, used by the AEIC-SE image codec)
|
||||||
|
// ships a per-ABI .so. arm64-v8a alone costs ~18 MB of APK; a
|
||||||
|
// universal APK carrying armeabi-v7a and x86_64 as well costs ~56 MB.
|
||||||
|
// * llamadart only declares android-arm64 and android-x64 backends in
|
||||||
|
// pubspec.yaml's `hooks.user_defines`, so an armeabi-v7a build already
|
||||||
|
// has no translation backend at all.
|
||||||
|
// * The image codec needs ~2.7 GiB peak resident, which no 32-bit
|
||||||
|
// address space can provide regardless of ABI.
|
||||||
|
//
|
||||||
|
// Consequence: this APK will not install on 32-bit-only ARM devices or on
|
||||||
|
// x86_64 emulators. For emulator work, temporarily add "x86_64" here.
|
||||||
|
ndk {
|
||||||
|
abiFilters += listOf("arm64-v8a")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -67,6 +80,13 @@ android {
|
|||||||
} else {
|
} else {
|
||||||
signingConfigs.getByName("debug")
|
signingConfigs.getByName("debug")
|
||||||
}
|
}
|
||||||
|
// ONNX Runtime resolves its Java classes from native code by name.
|
||||||
|
// Without these rules R8 renames them and the process SIGABRTs with
|
||||||
|
// "java_class == null" the instant the codec runs a model.
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# ONNX Runtime looks its Java classes up from native code with FindClass /
|
||||||
|
# GetMethodID, by literal name. R8 renames them, the lookup returns null, and
|
||||||
|
# the process dies with:
|
||||||
|
#
|
||||||
|
# JNI DETECTED ERROR IN APPLICATION: java_class == null
|
||||||
|
# at art::JNI<false>::GetMethodID
|
||||||
|
# from convertToTensorInfo -> Java_ai_onnxruntime_OrtSession_run
|
||||||
|
#
|
||||||
|
# It is a hard SIGABRT in native code, so nothing in Dart can catch it: the app
|
||||||
|
# vanishes the moment the codec touches the model. Keep the whole package —
|
||||||
|
# these are the types the native layer reflects on to build tensors and read
|
||||||
|
# results back.
|
||||||
|
-keep class ai.onnxruntime.** { *; }
|
||||||
|
-keepclassmembers class ai.onnxruntime.** { *; }
|
||||||
|
-dontwarn ai.onnxruntime.**
|
||||||
|
|
||||||
|
# The Flutter plugin's platform-channel handler, reached the same way.
|
||||||
|
-keep class com.masicai.flutteronnxruntime.** { *; }
|
||||||
|
-dontwarn com.masicai.flutteronnxruntime.**
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
|
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="meshcore_open"
|
android:label="MeshCore Open"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<service
|
<service
|
||||||
|
|||||||
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 183 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground>
|
||||||
|
<inset
|
||||||
|
android:drawable="@drawable/ic_launcher_foreground"
|
||||||
|
android:inset="16%" />
|
||||||
|
</foreground>
|
||||||
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#9F9D95</color>
|
||||||
|
</resources>
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||||
|
android.builtInKotlin=false
|
||||||
|
# This newDsl flag was added automatically by Flutter migrator
|
||||||
|
android.newDsl=false
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
|
||||||
|
This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
|
After Width: | Height: | Size: 669 KiB |
@@ -21,7 +21,12 @@ The MeshCore BLE protocol implements a binary frame-based communication system u
|
|||||||
|
|
||||||
### Connection Flow
|
### Connection Flow
|
||||||
|
|
||||||
1. **Scan** for devices with name prefix `MeshCore-`
|
1. **Scan** for devices with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`):
|
||||||
|
- `MeshCore-`
|
||||||
|
- `Whisper-`
|
||||||
|
- `WisCore-`
|
||||||
|
- `HT-`
|
||||||
|
- `LowMesh_MC_`
|
||||||
2. **Connect** with 15-second timeout
|
2. **Connect** with 15-second timeout
|
||||||
3. **Request MTU** of 185 bytes (falls back to default if unsupported)
|
3. **Request MTU** of 185 bytes (falls back to default if unsupported)
|
||||||
4. **Discover services** and locate NUS characteristics
|
4. **Discover services** and locate NUS characteristics
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ MeshCore Open is an open-source Flutter client for MeshCore LoRa mesh networking
|
|||||||
8. [Notifications](notifications.md) - System notifications, unread badges, and notification preferences
|
8. [Notifications](notifications.md) - System notifications, unread badges, and notification preferences
|
||||||
9. [Repeater Management](repeater-management.md) - Repeater hub, status, CLI, telemetry, and neighbors
|
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
|
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
|
11. [Routing Paths](routing-paths.md) - Path encoding, validation, device capability detection, and storage
|
||||||
|
12. [BLE Protocol & Data Layer](ble-protocol.md) - Technical reference for the communication protocol and data architecture
|
||||||
|
|
||||||
## App Overview
|
## App Overview
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ A bottom sheet with a search field and a grid of GIF thumbnails.
|
|||||||
### How to Access
|
### How to Access
|
||||||
App Settings → Appearance → Language
|
App Settings → Appearance → Language
|
||||||
|
|
||||||
### Supported Languages (15)
|
### Supported Languages (18)
|
||||||
English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian
|
English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian, Hungarian, Japanese, Korean
|
||||||
|
|
||||||
### How It Works
|
### How It Works
|
||||||
- All UI strings go through Flutter's ARB localization system
|
- All UI strings go through Flutter's ARB localization system
|
||||||
@@ -183,5 +183,80 @@ An ML-based service that predicts expected delivery timeouts:
|
|||||||
- Applies a **1.5x safety margin** to raw predictions (the actual timeout issued is 1.5× the model's predicted delivery time)
|
- 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
|
- Features with zero variance are automatically excluded from training
|
||||||
- Blends per-contact statistics with ML predictions
|
- Blends per-contact statistics with ML predictions
|
||||||
- Falls back to `3000 + 3000 × pathLength` ms when insufficient data
|
- Falls back to `3000 + 3000 × pathLength` ms when insufficient data. Note: `pathLength` here refers to the stored hop count in the app's model/storage (number of hops), not the on-air encoded byte length.
|
||||||
- Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost)
|
- Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## On-Device Message Translation
|
||||||
|
|
||||||
|
### What It Is
|
||||||
|
An optional on-device translation service powered by an embedded LLM (llamadart, running GGUF models). Translation runs entirely on-device — no data leaves the app.
|
||||||
|
|
||||||
|
### How to Access
|
||||||
|
Tap the translate button on any received message. On first use, the GGUF model file is downloaded and cached locally.
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
- Model files are managed by `TranslationFileStore`; download progress is shown in-place
|
||||||
|
- Before translating, the source language is automatically detected using the `flutter_langdetect` package. If the detected language already matches the target language, translation is skipped
|
||||||
|
- Translation runs via `TranslationService` using the llamadart CPU backend (arm64 and x64 on Android)
|
||||||
|
- Translated text is shown in `TranslatedMessageContent` as an inline overlay on the original message bubble
|
||||||
|
- Each translation is cached; re-tapping shows the cached result without re-running inference
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Emoji Reactions
|
||||||
|
|
||||||
|
### How to Access
|
||||||
|
Long-press a message bubble in any direct or channel chat, then select a reaction emoji.
|
||||||
|
|
||||||
|
### What the User Sees
|
||||||
|
An emoji picker inline with common reactions. Selected reactions appear below the message bubble with a count.
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
- Implemented via `emoji_picker.dart` and `reaction_helper.dart`
|
||||||
|
- Reactions are transmitted as a special message type visible to all participants with MeshCore Open
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Linkification
|
||||||
|
|
||||||
|
### What It Does
|
||||||
|
URLs and `meshcore://` URIs in received messages are automatically detected and rendered as tappable links.
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
- Powered by the `flutter_linkify` package via `link_handler.dart`
|
||||||
|
- Tapping a URL opens the system browser; tapping a `meshcore://` URI imports the contact
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GPX Export
|
||||||
|
|
||||||
|
### How to Access
|
||||||
|
Settings → Export section (three options: Export Repeaters, Export Contacts, Export All).
|
||||||
|
|
||||||
|
### What It Does
|
||||||
|
Exports contacts with GPS coordinates to a `.gpx` file via the OS share sheet. Not available on web.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pinch-to-Zoom Chat Text
|
||||||
|
|
||||||
|
### What It Does
|
||||||
|
Users can pinch to scale all chat text up or down within a session.
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
- Implemented via `ChatTextScaleService` and `ChatZoomWrapper`
|
||||||
|
- Scale range: 0.8× to 1.8×
|
||||||
|
- The chosen scale persists across the session via the service
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background Service (Android)
|
||||||
|
|
||||||
|
### What It Does
|
||||||
|
On Android, a foreground service (`background_service.dart`) keeps the BLE connection and message handling alive when the app is in the background. On other platforms this is a no-op.
|
||||||
|
|
||||||
|
### User Impact
|
||||||
|
- A persistent notification appears while the service is running
|
||||||
|
- Messages are received and retry logic continues even when the app is not in the foreground
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ RX (device → host): [0x3E][len_lo][len_hi][payload...]
|
|||||||
- Length: 2-byte little-endian, payload only
|
- Length: 2-byte little-endian, payload only
|
||||||
- Max payload: 172 bytes
|
- Max payload: 172 bytes
|
||||||
- TCP: `tcpNoDelay: true` (Nagle disabled), writes serialized to prevent interleaving
|
- TCP: `tcpNoDelay: true` (Nagle disabled), writes serialized to prevent interleaving
|
||||||
- USB: 10ms post-write delay between frames
|
|
||||||
|
|
||||||
## Connection State Machine
|
## Connection State Machine
|
||||||
|
|
||||||
@@ -49,8 +48,16 @@ enum MeshCoreConnectionState {
|
|||||||
|
|
||||||
## BLE Connection Lifecycle
|
## BLE Connection Lifecycle
|
||||||
|
|
||||||
1. **Scan** with keyword filters `["MeshCore-", "Whisper-"]`
|
1. **Scan** with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`):
|
||||||
2. **Connect** with 15-second timeout
|
- `MeshCore-`
|
||||||
|
- `Whisper-`
|
||||||
|
- `WisCore-`
|
||||||
|
- `Seeed`
|
||||||
|
- `Lilygo`
|
||||||
|
- `HT-`
|
||||||
|
- `LowMesh_MC_`
|
||||||
|
- `NRF52`
|
||||||
|
2. **Connect** with 15-second timeout (6 seconds on Linux)
|
||||||
3. **Request MTU** 185 bytes (non-web only)
|
3. **Request MTU** 185 bytes (non-web only)
|
||||||
4. **Discover services** and locate NUS
|
4. **Discover services** and locate NUS
|
||||||
5. **Enable TX notifications** (up to 3 attempts on native)
|
5. **Enable TX notifications** (up to 3 attempts on native)
|
||||||
@@ -74,7 +81,7 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
|
|||||||
| Max path size | 64 bytes | Maximum path data |
|
| Max path size | 64 bytes | Maximum path data |
|
||||||
| Max name size | 32 bytes | Maximum node name |
|
| Max name size | 32 bytes | Maximum node name |
|
||||||
| Max text payload | 160 bytes | Firmware `MAX_TEXT_LEN` |
|
| Max text payload | 160 bytes | Firmware `MAX_TEXT_LEN` |
|
||||||
| App protocol version | 3 | Sent in device query |
|
| App protocol version | 4 | Sent in device query |
|
||||||
| Contact frame size | 148 bytes | Fixed-size contact record |
|
| Contact frame size | 148 bytes | Fixed-size contact record |
|
||||||
|
|
||||||
## Command Codes (App → Device)
|
## Command Codes (App → Device)
|
||||||
@@ -109,13 +116,17 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
|
|||||||
| 32 | CMD_SET_CHANNEL | Set channel name and PSK |
|
| 32 | CMD_SET_CHANNEL | Set channel name and PSK |
|
||||||
| 36 | CMD_SEND_TRACE_PATH | Request path trace |
|
| 36 | CMD_SEND_TRACE_PATH | Request path trace |
|
||||||
| 38 | CMD_SET_OTHER_PARAMS | Set misc parameters |
|
| 38 | CMD_SET_OTHER_PARAMS | Set misc parameters |
|
||||||
| 39 | CMD_GET_TELEMETRY_REQ | Request sensor telemetry |
|
| 39 | CMD_SEND_TELEMETRY_REQ | Request sensor telemetry |
|
||||||
| 40 | CMD_GET_CUSTOM_VAR | Get custom variables |
|
| 40 | CMD_GET_CUSTOM_VAR | Get custom variables |
|
||||||
| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable |
|
| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable |
|
||||||
| 50 | CMD_SEND_BINARY_REQ | Send binary request |
|
| 50 | CMD_SEND_BINARY_REQ | Send binary request |
|
||||||
|
| 54 | CMD_SET_FLOOD_SCOPE | Set flood routing scope (v8+) |
|
||||||
|
| 55 | CMD_SEND_CONTROL_DATA | Send control data (e.g. zero-hop discovery, v8+) |
|
||||||
|
| 56 | CMD_GET_STATS | Request companion radio stats |
|
||||||
| 57 | CMD_SEND_ANON_REQ | Send anonymous request |
|
| 57 | CMD_SEND_ANON_REQ | Send anonymous request |
|
||||||
| 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration |
|
| 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration |
|
||||||
| 59 | CMD_GET_AUTO_ADD_CONFIG | Get auto-add configuration |
|
| 59 | CMD_GET_AUTO_ADD_CONFIG | Get auto-add configuration |
|
||||||
|
| 61 | CMD_SET_PATH_HASH_MODE | Set path hash width (bytes per hop) |
|
||||||
|
|
||||||
## Response / Push Codes (Device → App)
|
## Response / Push Codes (Device → App)
|
||||||
|
|
||||||
@@ -139,6 +150,7 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
|
|||||||
| 17 | RESP_CODE_CHANNEL_MSG_RECV_V3 | Incoming channel message (v3) |
|
| 17 | RESP_CODE_CHANNEL_MSG_RECV_V3 | Incoming channel message (v3) |
|
||||||
| 18 | RESP_CODE_CHANNEL_INFO | Channel definition |
|
| 18 | RESP_CODE_CHANNEL_INFO | Channel definition |
|
||||||
| 21 | RESP_CODE_CUSTOM_VARS | Custom variables |
|
| 21 | RESP_CODE_CUSTOM_VARS | Custom variables |
|
||||||
|
| 24 | RESP_CODE_STATS | Companion radio stats |
|
||||||
| 25 | RESP_CODE_AUTO_ADD_CONFIG | Auto-add flags |
|
| 25 | RESP_CODE_AUTO_ADD_CONFIG | Auto-add flags |
|
||||||
| 0x80 | PUSH_CODE_ADVERT | Known contact re-seen |
|
| 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 |
|
| 0x81 | PUSH_CODE_PATH_UPDATED | Better path found; carries the 32-byte public key of the updated contact |
|
||||||
@@ -152,6 +164,7 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
|
|||||||
| 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered |
|
| 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered |
|
||||||
| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data |
|
| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data |
|
||||||
| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response |
|
| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response |
|
||||||
|
| 0x8E | PUSH_CODE_CONTROL_DATA | Control data push (e.g. zero-hop discovery response) |
|
||||||
|
|
||||||
## Data Models
|
## Data Models
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
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.
|
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.
|
The number of active channels is determined by the firmware (default 40); the device reports its actual limit at login.
|
||||||
|
|
||||||
## How to Access
|
## How to Access
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ QuickSwitchBar tab 1 (middle) from any main screen.
|
|||||||
| Public | Globe | Green | Fixed well-known PSK; any device can join |
|
| 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 |
|
| 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 |
|
| 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 |
|
| Community | Groups/Tag | Magenta | PSK derived via HMAC-SHA256 from a community's shared secret |
|
||||||
|
|
||||||
## Channels List Screen
|
## Channels List Screen
|
||||||
|
|
||||||
@@ -26,13 +26,12 @@ QuickSwitchBar tab 1 (middle) from any main screen.
|
|||||||
- **Search bar** with live text filtering (300ms debounce)
|
- **Search bar** with live text filtering (300ms debounce)
|
||||||
- **Sort/filter button**
|
- **Sort/filter button**
|
||||||
- **Scrollable list of channel cards**, each showing:
|
- **Scrollable list of channel cards**, each showing:
|
||||||
- Type icon with color coding (purple badge overlay for community channels)
|
- Type icon with color coding (magenta badge overlay for community channels)
|
||||||
- Channel name (or "Channel N" if unnamed)
|
- Channel name (or "Channel N" if unnamed)
|
||||||
- Subtitle: "Public channel", "Hashtag channel", "Private channel", or "Community channel - {name}"
|
|
||||||
- Unread badge (if messages are unread)
|
- Unread badge (if messages are unread)
|
||||||
- Drag handle (when manual sort is active)
|
- Drag handle (when manual sort is active)
|
||||||
- **"+" FAB** to add a new channel
|
- **"+" FAB** to add a new channel
|
||||||
- **Overflow menu**: Disconnect, Manage Communities (only shown when at least one community exists), Settings
|
- **Overflow menu**: Disconnect, Manage Communities, 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.
|
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.
|
||||||
|
|
||||||
@@ -60,7 +59,7 @@ Tap the "+" FAB to open a dialog with six options:
|
|||||||
|
|
||||||
| Action | Description |
|
| 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) |
|
| Edit | Change name, PSK (with a dice icon to generate a random PSK), SMAZ compression toggle (compresses outgoing messages to allow longer text within the byte limit), or Cyr2Lat encoding toggle (transliterates Cyrillic to Latin for compatibility) |
|
||||||
| Mute / Unmute | Toggle push notification suppression for this channel |
|
| Mute / Unmute | Toggle push notification suppression for this channel |
|
||||||
| Delete | Remove the channel from the device (confirmation required) |
|
| Delete | Remove the channel from the device (confirmation required) |
|
||||||
|
|
||||||
@@ -70,9 +69,9 @@ Tap a channel card to open the channel chat screen.
|
|||||||
|
|
||||||
### App Bar
|
### App Bar
|
||||||
|
|
||||||
- Type icon (public/private/hashtag)
|
- Type icon: globe for public channels, tag (#) for all other channel types
|
||||||
- Channel name
|
- Channel name
|
||||||
- Subtitle: "{type} - {N} unread"
|
- Subtitle: "{Public|Private} • {N} unread" (e.g., "Public • 3 unread")
|
||||||
|
|
||||||
### Message Display
|
### Message Display
|
||||||
|
|
||||||
@@ -101,8 +100,7 @@ Tap a channel card to open the channel chat screen.
|
|||||||
|
|
||||||
### Message Path Viewing
|
### Message Path Viewing
|
||||||
|
|
||||||
- **Mobile**: Tap a message bubble to view its routing path
|
- **All platforms**: Long-press (or right-click on desktop) a message bubble → "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))
|
- Opens the Channel Message Path Screen (see [Additional Features](additional-features.md))
|
||||||
|
|
||||||
### Context Actions (Long-Press / Right-Click)
|
### Context Actions (Long-Press / Right-Click)
|
||||||
@@ -110,20 +108,12 @@ Tap a channel card to open the channel chat screen.
|
|||||||
| Action | Availability | Description |
|
| Action | Availability | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Reply | All messages | Triggers reply mode |
|
| Reply | All messages | Triggers reply mode |
|
||||||
| Path | Desktop only | Opens message path view |
|
| Path | All messages | Opens message path view |
|
||||||
| Add Reaction | Incoming messages only | Opens emoji picker (cannot react to your own messages) |
|
| Add Reaction | Incoming messages only | Opens emoji picker (cannot react to your own messages) |
|
||||||
| Copy | All messages | Copies text to clipboard |
|
| Copy | All messages | Copies text to clipboard |
|
||||||
|
| Mark as Unread | Incoming messages only | Marks this message and all subsequent incoming messages as unread |
|
||||||
| Delete | All messages | Removes locally (not from mesh) |
|
| 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
|
||||||
|
|
||||||
Communities are a layer above channels that provide a private namespace.
|
Communities are a layer above channels that provide a private namespace.
|
||||||
@@ -151,7 +141,7 @@ From the channels screen overflow menu → "Manage Communities". Opens a draggab
|
|||||||
- **Tap a community** to directly show its QR code for sharing
|
- **Tap a community** to directly show its QR code for sharing
|
||||||
- **Popup menu** per community:
|
- **Popup menu** per community:
|
||||||
- **Show QR** — displays the QR code for sharing with new members
|
- **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)
|
- **Leave Community** — 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
|
## How Channels Differ from Direct Messages
|
||||||
|
|
||||||
|
|||||||
@@ -18,17 +18,15 @@ From the Contacts screen, tap any Chat-type contact to open the ChatScreen.
|
|||||||
|
|
||||||
- **Title**: Contact name
|
- **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.
|
- **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**:
|
- **Action button**:
|
||||||
- **Routing mode** (waves icon): Switch between Auto, Direct, and Flood routing
|
- **Overflow menu** (⋮ icon): Contains Routing, Info, Telemetry, Settings, and Clear Chat. Routing opens the routing sheet where you can switch between Auto, Direct, and Flood routing and manage recent paths (hop count, round-trip time, age, success count, color-coded by repeater). Info shows a dialog with contact type, path, GPS coordinates, and public key.
|
||||||
- **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
|
### Message List
|
||||||
|
|
||||||
- Scrollable list with newest messages at the bottom
|
- 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
|
- **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)
|
- **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
|
- Bubble width capped at 72% of screen width
|
||||||
- Hyperlinks rendered as tappable green underlined text
|
- Hyperlinks rendered as tappable green underlined text
|
||||||
- **Pinch-to-zoom**: Two-finger zoom (0.8x–1.8x) and double-tap to reset
|
- **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
|
- **Jump to bottom**: Floating button appears when scrolled away from the bottom
|
||||||
@@ -37,6 +35,7 @@ From the Contacts screen, tap any Chat-type contact to open the ChatScreen.
|
|||||||
### Input Bar
|
### Input Bar
|
||||||
|
|
||||||
- **GIF button** (left): Opens GIF picker bottom sheet
|
- **GIF button** (left): Opens GIF picker bottom sheet
|
||||||
|
- **Translation button** (optional, between GIF and text field): Shown only when translation is enabled in App Settings. Tap to configure outgoing-message translation language and on/off toggle.
|
||||||
- **Text field** (center): Auto-capitalization, enforces UTF-8 byte limit in real-time
|
- **Text field** (center): Auto-capitalization, enforces UTF-8 byte limit in real-time
|
||||||
- **Send button** (right): Submits the message
|
- **Send button** (right): Submits the message
|
||||||
- On desktop: Enter/Numpad Enter also submits
|
- On desktop: Enter/Numpad Enter also submits
|
||||||
@@ -66,8 +65,8 @@ Outgoing messages display a status indicator:
|
|||||||
|
|
||||||
When enabled in App Settings, additional metadata appears inside each bubble:
|
When enabled in App Settings, additional metadata appears inside each bubble:
|
||||||
- Timestamp (HH:MM)
|
- Timestamp (HH:MM)
|
||||||
- Retry count (e.g., "Retry 2 of 4")
|
- Retry count (e.g., "Retry 2 of 4") — only shown for outgoing messages where at least one retry has occurred
|
||||||
- Status icon
|
- Status icon (outgoing only)
|
||||||
- Round-trip time in seconds (if delivered)
|
- Round-trip time in seconds (if delivered)
|
||||||
|
|
||||||
## Message Length Limits
|
## Message Length Limits
|
||||||
@@ -86,7 +85,7 @@ When a direct message is sent:
|
|||||||
|
|
||||||
1. The app computes an expected ACK hash: `SHA256([timestamp][attempt][text][selfPubKey])[0:4]` — matching the firmware's hash calculation. If SMAZ compression is enabled, the compressed text (not the original) is hashed
|
1. The app computes an expected ACK hash: `SHA256([timestamp][attempt][text][selfPubKey])[0:4]` — matching the firmware's hash calculation. If SMAZ compression is enabled, the compressed text (not the original) is hashed
|
||||||
2. On device acknowledgment (`RESP_CODE_SENT`), the message transitions to "sent" and a timeout timer starts
|
2. On device acknowledgment (`RESP_CODE_SENT`), the message transitions to "sent" and a timeout timer starts
|
||||||
3. **Timeout duration**: Preferably from the ML timeout prediction service; otherwise `3000 + 3000 × path_length` milliseconds (15000ms for flood)
|
3. **Timeout duration**: Preferably from the ML timeout prediction service; otherwise from the device's own `est_timeout` in `RESP_CODE_SENT` (clamped to the physics range); otherwise calculated from LoRa airtime physics: `500 + (airtime × 6 + 250) × (pathLength + 1)` ms for direct paths, `500 + 16 × airtime` ms for flood (airtime is estimated from the radio's current spreading factor, bandwidth, and coding rate). The result is capped at 45 seconds.
|
||||||
4. On timeout, the message is retried with **exponential backoff**: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s, 16s...)
|
4. On timeout, the message is retried with **exponential backoff**: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s, 16s...)
|
||||||
5. **Max retries**: Configurable (default 5, range 2–10)
|
5. **Max retries**: Configurable (default 5, range 2–10)
|
||||||
6. After max retries, the message is marked "failed" — but a **30-second grace window** remains during which a late ACK can still resolve the message to "delivered"
|
6. After max retries, the message is marked "failed" — but a **30-second grace window** remains during which a late ACK can still resolve the message to "delivered"
|
||||||
@@ -113,8 +112,10 @@ Add emoji reactions to incoming messages (not your own):
|
|||||||
| Action | Availability | Description |
|
| Action | Availability | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Add reaction | Incoming messages only | Opens emoji picker |
|
| Add reaction | Incoming messages only | Opens emoji picker |
|
||||||
| View path | Mobile: tap bubble directly; Desktop: long-press/right-click menu | Shows message routing path |
|
| View path | All platforms: long-press/right-click menu | Shows message routing path |
|
||||||
| Copy | All messages | Copies text to clipboard |
|
| Copy | All messages | Copies text to clipboard |
|
||||||
|
| Translate | Incoming messages only (when translation is enabled and not yet translated) | Translates the message on-demand using the on-device model |
|
||||||
|
| Mark as Unread | Incoming messages only | Marks this message and all subsequent incoming messages as unread |
|
||||||
| Delete | All messages | Removes locally (not from mesh) |
|
| Delete | All messages | Removes locally (not from mesh) |
|
||||||
| Retry | Failed outgoing messages | Re-sends the message |
|
| Retry | Failed outgoing messages | Re-sends the message |
|
||||||
| Open chat with sender | Room server chats | Opens 1:1 chat with the message sender |
|
| Open chat with sender | Room server chats | Opens 1:1 chat with the message sender |
|
||||||
|
|||||||
@@ -6,18 +6,17 @@ The Contacts screen is the primary hub for managing mesh nodes your radio has a
|
|||||||
|
|
||||||
## How to Access
|
## How to Access
|
||||||
|
|
||||||
- Automatically shown after connecting to a device
|
- QuickSwitchBar tab 0 (leftmost) from Channels or Map screens (Channels is shown first after connecting)
|
||||||
- QuickSwitchBar tab 0 (leftmost) from Channels or Map screens
|
|
||||||
- Back navigation from Chat or Settings screens
|
- Back navigation from Chat or Settings screens
|
||||||
|
|
||||||
## Contact Types
|
## Contact Types
|
||||||
|
|
||||||
| Type | Avatar Color | Icon | Description |
|
| Type | Avatar Color | Icon | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Chat | Blue | Chat bubble | Another user's mesh radio |
|
| Chat | Blue | Initials / emoji | Another user's mesh radio |
|
||||||
| Repeater | Orange | Cell tower | A mesh repeater/relay node |
|
| Repeater | Amber | Cell tower | A mesh repeater/relay node |
|
||||||
| Room | Purple | Group | A room server for group chat |
|
| Room | Magenta | Meeting room | A room server for group chat |
|
||||||
| Sensor | Green | Sensors | A sensor device |
|
| Sensor | Teal | Sensors | A sensor device |
|
||||||
|
|
||||||
## Contact List
|
## Contact List
|
||||||
|
|
||||||
@@ -73,41 +72,42 @@ Groups are stored per radio identity (scoped by public key).
|
|||||||
|
|
||||||
| Action | Availability | Description |
|
| Action | Availability | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Path Trace / Ping | Repeaters, Rooms (always); Chat if `pathLength > 0` | Opens PathTraceMapScreen. Label shows "Ping" when no path bytes are known, "Path Trace" otherwise |
|
| Ping | Repeaters only | Opens PathTraceMapScreen targeting the repeater |
|
||||||
|
| Path Trace | Rooms (always); Chat/Sensor only if `pathLength > 0` | Opens PathTraceMapScreen. For rooms, label shows "Ping" when no path bytes are known, "Path Trace" when path bytes are available |
|
||||||
| Manage Repeater | Repeaters only | Login dialog → RepeaterHubScreen |
|
| Manage Repeater | Repeaters only | Login dialog → RepeaterHubScreen |
|
||||||
| Room Login | Rooms only | Login dialog → ChatScreen |
|
| Room Login | Rooms only | Login dialog → ChatScreen |
|
||||||
| Room Management | Rooms only | Login dialog → RepeaterHubScreen (management mode) |
|
| Room Management | Rooms only | Login dialog → RepeaterHubScreen (management mode) |
|
||||||
| Open Chat | Chat/Sensor | Same as single tap |
|
|
||||||
| Add/Remove Favorite | All types | Toggles the favorite flag |
|
| Add/Remove Favorite | All types | Toggles the favorite flag |
|
||||||
| Share Contact | All types | Copies `meshcore://<hex>` URI to clipboard |
|
| Share Contact | All types | Requests advert from device → copies `meshcore://<hex>` URI to clipboard |
|
||||||
| Share Contact Zero-Hop | All types | Broadcasts the contact's advertisement one hop |
|
| Share Contact Zero-Hop | All types | Broadcasts the contact's advertisement one hop |
|
||||||
| Delete Contact | All types | Confirmation dialog → removes from device and clears messages |
|
| Delete Contact | All types | Confirmation dialog → removes from device and clears messages |
|
||||||
|
|
||||||
## App Bar Menus
|
## App Bar Menus
|
||||||
|
|
||||||
The Contacts screen has **two separate popup menus** in the app bar:
|
The Contacts screen has a single **three-dot overflow menu** (`⋮`) in the app bar:
|
||||||
|
|
||||||
**Antenna icon menu** (contact sharing):
|
- Discovered Contacts — opens the DiscoveryScreen
|
||||||
|
- Add Contact from Clipboard — reads a `meshcore://<hex>` URI from clipboard and imports it
|
||||||
|
- *(divider)*
|
||||||
- Zero-Hop Advert — broadcasts your advertisement to immediately adjacent nodes
|
- Zero-Hop Advert — broadcasts your advertisement to immediately adjacent nodes
|
||||||
- Flood Advert — broadcasts across the full mesh network
|
- Flood Advert — broadcasts across the full mesh network
|
||||||
- Copy Advert to Clipboard — copies your `meshcore://<hex>` URI for sharing externally
|
- Copy Advert to Clipboard — copies your `meshcore://<hex>` URI for sharing externally
|
||||||
- Add Contact from Clipboard — reads a `meshcore://<hex>` URI from clipboard and imports it
|
- *(divider)*
|
||||||
|
|
||||||
**Three-dot overflow menu**:
|
|
||||||
- Disconnect — disconnects from the device
|
- Disconnect — disconnects from the device
|
||||||
- Discovered Contacts — opens the DiscoveryScreen
|
|
||||||
- Settings — opens the Settings screen
|
- Settings — opens the Settings screen
|
||||||
|
|
||||||
|
A **floating action button** (person-add icon) provides a shortcut sheet to "Add Contact from Clipboard" or "Discovered Contacts".
|
||||||
|
|
||||||
## Adding Contacts
|
## Adding Contacts
|
||||||
|
|
||||||
### Automatic (Passive)
|
### Automatic (Passive)
|
||||||
When the radio hears an advertisement, the contact appears automatically if auto-add is enabled for that type (configurable in Settings → Contact Settings).
|
When the radio hears an advertisement, the contact appears automatically if auto-add is enabled for that type (configurable in Settings → Contact Settings).
|
||||||
|
|
||||||
### Import from Clipboard
|
### Import from Clipboard
|
||||||
Antenna menu → "Add Contact from Clipboard". Reads a `meshcore://<hex>` URI from clipboard and imports it to the device.
|
Overflow menu (or the FAB shortcut) → "Add Contact from Clipboard". Reads a `meshcore://<hex>` URI from clipboard and imports it to the device.
|
||||||
|
|
||||||
### Import from Discovered Contacts
|
### Import from Discovered Contacts
|
||||||
Overflow menu → "Discovered Contacts". Shows nodes heard passively that haven't been added yet. Tap to immediately import (no confirmation dialog), or long-press for more options (Add, Copy URI, Delete). The Discovery screen has its own search bar, type filters (Users, Repeaters, Rooms, Favorites), and sort options (Last Seen, A-Z). An overflow "Delete All" option clears all discovered contacts.
|
Overflow menu → "Discovered Contacts". Shows nodes heard passively that haven't been added yet. Tap to immediately import (no confirmation dialog), or long-press for more options (Copy URI, Delete). The Discovery screen has its own search bar, type filters (Users, Repeaters, Rooms), and sort options (Last Seen, A-Z). An overflow "Delete All" option clears all discovered contacts.
|
||||||
|
|
||||||
## Contact Sharing Format
|
## Contact Sharing Format
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ All contacts with known GPS coordinates are plotted:
|
|||||||
| Room | Purple | Meeting room |
|
| Room | Purple | Meeting room |
|
||||||
| Sensor | Orange | Sensors |
|
| Sensor | Orange | Sensors |
|
||||||
|
|
||||||
Node name labels appear automatically at zoom level 12 and above.
|
Node name labels appear automatically at zoom level 14 and above.
|
||||||
|
|
||||||
### Shared Map Pins (Flag Icons)
|
### Shared Map Pins (Flag Icons)
|
||||||
Location pins shared in chat messages are displayed as flags:
|
Location pins shared in chat messages are displayed as flags:
|
||||||
@@ -35,9 +35,9 @@ Location pins shared in chat messages are displayed as flags:
|
|||||||
|
|
||||||
Tap a pin to see its info. Options to "Hide" (session only) or "Remove" (persistent).
|
Tap a pin to see its info. Options to "Hide" (session only) or "Remove" (persistent).
|
||||||
|
|
||||||
### Predicted / Guessed Locations (Semi-Transparent)
|
### Predicted / Guessed Locations
|
||||||
|
|
||||||
Many contacts on the mesh don't have GPS hardware, so the map has no explicit coordinates for them. Instead of leaving these contacts invisible, the app **infers an approximate position** by analyzing the repeater path the contact's messages travel through. These inferred positions are displayed as semi-transparent markers with a `not_listed_location` icon, visually distinct from confirmed-location markers.
|
Many contacts on the mesh don't have GPS hardware, so the map has no explicit coordinates for them. Instead of leaving these contacts invisible, the app **infers an approximate position** by analyzing the repeater path the contact's messages travel through. These inferred positions are displayed as markers with a `not_listed_location` icon and a muted grey or colored border, visually distinct from confirmed-location markers.
|
||||||
|
|
||||||
#### Why guessed locations exist
|
#### Why guessed locations exist
|
||||||
|
|
||||||
@@ -55,19 +55,19 @@ In a mesh network, every message hops through one or more repeaters on its way t
|
|||||||
|
|
||||||
5. **Compute the estimated position**:
|
5. **Compute the estimated position**:
|
||||||
- **Single anchor**: The contact is placed on a small circle (330m radius) around the repeater. The angle on the circle is deterministic — derived from an FNV-1a hash of the contact's public key — so the same contact always appears at the same offset, preventing markers from stacking on top of each other.
|
- **Single anchor**: The contact is placed on a small circle (330m radius) around the repeater. The angle on the circle is deterministic — derived from an FNV-1a hash of the contact's public key — so the same contact always appears at the same offset, preventing markers from stacking on top of each other.
|
||||||
- **Two or more anchors**: The position is the average (centroid) of all anchor coordinates, with a smaller offset radius (80–120m) applied for visual separation.
|
- **Two or more anchors**: The position is a weighted average of all anchor coordinates (each subsequent anchor weighted at half the previous one, biasing toward the first), with a smaller offset radius (120m for 2 anchors, 80m for 3+) applied for visual separation.
|
||||||
|
|
||||||
6. **Assign confidence level**:
|
6. **Assign confidence level**:
|
||||||
- **High confidence** (2+ anchors): Displayed at 55% opacity.
|
- **High confidence** (2+ anchors): The marker border uses the node's type color (brighter border).
|
||||||
- **Low confidence** (1 anchor): Displayed at 30% opacity.
|
- **Low confidence** (1 anchor): The marker border is rendered in a muted grey.
|
||||||
|
|
||||||
7. **Cache the result**: The computation is cached using a key derived from the contact's paths, anchor positions, path-history version, and radio parameters. The cache is only invalidated when any of these inputs change, avoiding recomputation on every UI rebuild.
|
7. **Cache the result**: The computation is cached using a key derived from the contact's paths, anchor positions, path-history version, and radio parameters. The cache is only invalidated when any of these inputs change, avoiding recomputation on every UI rebuild.
|
||||||
|
|
||||||
#### How to read guessed locations on the map
|
#### How to read guessed locations on the map
|
||||||
|
|
||||||
- **Semi-transparent marker** with a `not_listed_location` icon: This is a guessed position, not a confirmed GPS fix.
|
- **Marker with `not_listed_location` icon**: This is a guessed position, not a confirmed GPS fix.
|
||||||
- **More opaque** (55%): Higher confidence — the contact was seen through 2 or more repeaters with known positions.
|
- **Colored border** (type color): Higher confidence — the contact was seen through 2 or more repeaters with known positions.
|
||||||
- **More transparent** (30%): Lower confidence — based on a single repeater anchor only.
|
- **Grey border**: Lower confidence — based on a single repeater anchor only.
|
||||||
- Coordinates shown in the marker info dialog are prefixed with `~` to indicate they are estimated.
|
- Coordinates shown in the marker info dialog are prefixed with `~` to indicate they are estimated.
|
||||||
- Guessed locations can be toggled on/off in the map filter dialog (FAB → "Guessed locations" toggle).
|
- Guessed locations can be toggled on/off in the map filter dialog (FAB → "Guessed locations" toggle).
|
||||||
|
|
||||||
@@ -88,10 +88,10 @@ Shows a bottom sheet with:
|
|||||||
- **Set as my location**: Updates your device's advertised location
|
- **Set as my location**: Updates your device's advertised location
|
||||||
|
|
||||||
### Filter Dialog (FAB)
|
### Filter Dialog (FAB)
|
||||||
Toggle visibility of: chat nodes, repeaters, other nodes, guessed locations, discovery contacts.
|
Toggle visibility of: chat nodes, repeaters, other nodes, guessed locations, discovery contacts, overlapping markers (stacked markers at similar coordinates), and shared map pins (flag markers).
|
||||||
Additional filters:
|
Additional filters:
|
||||||
- **Key prefix filter**: Show only contacts whose public key starts with a given prefix
|
- **Key prefix filter**: Show only contacts whose public key starts with a given prefix
|
||||||
- **Last-seen time slider**: From 1 hour to "all time"
|
- **Last-seen time slider**: Exponential scale from near-zero to 6 months, with "all time" at the top end
|
||||||
|
|
||||||
### Legend Card (Top-Right)
|
### Legend Card (Top-Right)
|
||||||
Shows node count and pin count. Tappable to expand a legend of all marker types.
|
Shows node count and pin count. Tappable to expand a legend of all marker types.
|
||||||
@@ -110,9 +110,16 @@ A map with a polyline showing the route from your node through repeater hops to
|
|||||||
- **Green circles**: Hops with known GPS coordinates
|
- **Green circles**: Hops with known GPS coordinates
|
||||||
- **Orange circles** (`~HH`): Inferred positions (no GPS but deducible from contacts)
|
- **Orange circles** (`~HH`): Inferred positions (no GPS but deducible from contacts)
|
||||||
- **Red endpoint**: Target contact with known GPS
|
- **Red endpoint**: Target contact with known GPS
|
||||||
- **Purple semi-transparent endpoint**: Target with guessed position
|
- **Magenta endpoint**: Target with guessed position
|
||||||
|
|
||||||
A legend card at the bottom lists each hop pair with SNR quality icons and total path distance.
|
A bottom panel shows each hop pair with SNR quality icons and total path distance. When multiple observed paths are available, a **Single / Combined** toggle appears at the top of the map. In Combined view, all paths are overlaid; shared segments are highlighted with a white halo and a path count badge appears on shared nodes.
|
||||||
|
|
||||||
|
The bottom panel also provides **packet animation controls**:
|
||||||
|
- **Animation toggle** (on/off)
|
||||||
|
- **Step back / Play / Step forward / Replay** buttons
|
||||||
|
- **Follow packet lock** — keeps the map camera centered on the moving packet dot
|
||||||
|
- **Speed selector** (0.5×, 1×, 2×, 4×)
|
||||||
|
- A live **"Hop x of y · from → to"** label that tracks the active segment
|
||||||
|
|
||||||
### How It Works
|
### How It Works
|
||||||
Sends a trace request frame over the mesh. The repeater network traces the path hop-by-hop and returns per-hop SNR data. For hops without GPS, positions are inferred by averaging GPS coordinates of contacts sharing that last-hop byte.
|
Sends a trace request frame over the mesh. The repeater network traces the path hop-by-hop and returns per-hop SNR data. For hops without GPS, positions are inferred by averaging GPS coordinates of contacts sharing that last-hop byte.
|
||||||
@@ -125,17 +132,17 @@ Sends a trace request frame over the mesh. The repeater network traces the path
|
|||||||
From the main map, tap the terrain/antenna icon.
|
From the main map, tap the terrain/antenna icon.
|
||||||
|
|
||||||
### What the User Sees
|
### What the User Sees
|
||||||
A full-screen map with a collapsible control panel containing:
|
A full-screen map with a draggable bottom sheet containing:
|
||||||
- **Elevation profile chart**: Terrain fill (green), LOS beam line (white), radio horizon line (yellow)
|
- **Elevation profile chart**: Terrain fill (green), LOS beam line (white), radio horizon line (yellow); obstruction points are marked as clickable dots on the chart
|
||||||
- **Status**: Clear (green) or blocked (red) with distance and minimum clearance
|
- **Status summary**: Clear (green), Marginal (amber, within 5 m of obstruction), or Blocked (red) with distance and clearance/obstruction amount
|
||||||
- **Options panel**: Node toggles, endpoint dropdowns, antenna height sliders (0–400 ft), Run LOS button
|
- **Options section** (collapsible): Node toggles, endpoint dropdowns, antenna height sliders (0–400 ft), Run LOS button
|
||||||
|
|
||||||
### Key Interactions
|
### Key Interactions
|
||||||
- **Long-press the map** to add custom endpoints (orange pushpin markers, renameable/deleteable)
|
- **Long-press the map** to add custom endpoints (pushpin markers, renameable/deleteable)
|
||||||
- **Tap a marker** to select it as Point A or B; LOS runs automatically when both are set
|
- **Tap a marker** to select it as Point A or B; LOS runs automatically when both are set
|
||||||
- **Antenna heights** are adjustable for both endpoints
|
- **Antenna heights** are adjustable for both endpoints
|
||||||
- **Map line** between endpoints is colored green (clear) or red (blocked)
|
- **Map line** between endpoints is colored green (clear), amber (marginal), or red (blocked)
|
||||||
- Terrain elevation is fetched from the Open-Meteo API (21–81 sample points, cached 24 hours)
|
- Terrain elevation is fetched from the Open-Meteo API (21, 41, or 81 sample points depending on link distance, cached 24 hours)
|
||||||
- K-factor is adjusted per radio frequency from a baseline of 4/3 at 915 MHz
|
- K-factor is adjusted per radio frequency from a baseline of 4/3 at 915 MHz
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -149,7 +156,7 @@ Settings → App Settings → Map Display → Offline Map Cache
|
|||||||
- Map with a blue polygon overlay showing previously selected cache bounds
|
- Map with a blue polygon overlay showing previously selected cache bounds
|
||||||
- Bounding box coordinates card
|
- Bounding box coordinates card
|
||||||
- **Cache Area** controls: "Use Current View" and Clear buttons
|
- **Cache Area** controls: "Use Current View" and Clear buttons
|
||||||
- **Zoom Range** slider (3–18) with estimated tile count
|
- **Zoom Range** range slider (3–18, dual-handle for min and max) with estimated tile count
|
||||||
- **Download progress** bar (when downloading)
|
- **Download progress** bar (when downloading)
|
||||||
- **Download Tiles** and **Clear Cache** buttons
|
- **Download Tiles** and **Clear Cache** buttons
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
The app follows this general flow:
|
The app follows this general flow:
|
||||||
|
|
||||||
```
|
```
|
||||||
Launch → Scanner Screen → [Connect via BLE/USB/TCP] → Contacts Screen
|
Launch → Scanner Screen → [Connect via BLE/USB/TCP] → Channels Screen
|
||||||
```
|
```
|
||||||
|
|
||||||
After connecting, the three main screens (Contacts, Channels, Map) are accessible via a persistent bottom navigation bar called the **QuickSwitchBar**.
|
After connecting, the three main screens (Contacts, Channels, Map) are accessible via a persistent bottom navigation bar called the **QuickSwitchBar**.
|
||||||
@@ -22,42 +22,27 @@ The QuickSwitchBar is a Material 3 `NavigationBar` with a frosted-glass visual t
|
|||||||
|
|
||||||
Tapping a tab replaces the current screen with a subtle fade + slight horizontal nudge transition (220ms forward, 200ms reverse). The back button is suppressed on all three main screens — navigation between them is flat, not stacked. All icons use outline variants (`people_outline`, `tag`, `map_outlined`) following Material 3 conventions.
|
Tapping a tab replaces the current screen with a subtle fade + slight horizontal nudge transition (220ms forward, 200ms reverse). The back button is suppressed on all three main screens — navigation between them is flat, not stacked. All icons use outline variants (`people_outline`, `tag`, `map_outlined`) following Material 3 conventions.
|
||||||
|
|
||||||
## Device Screen
|
## Disconnection
|
||||||
|
|
||||||
The Device Screen is a transitional hub that shows after connection. In practice, the app navigates directly to Contacts after connecting, but the Device Screen is reachable via the QuickSwitchBar.
|
- The disconnect button (available in the overflow menu of each main screen) shows a confirmation dialog before disconnecting
|
||||||
|
|
||||||
### What the User Sees
|
|
||||||
|
|
||||||
**App Bar**:
|
|
||||||
- Left: Battery indicator chip (tappable — toggles between percentage and voltage display). Icon changes based on level: `battery_unknown` when data unavailable, `battery_alert` (orange) at 15% or below, `battery_full` otherwise
|
|
||||||
- Left-aligned title (`centerTitle: false`): Two-line layout — small grey "MeshCore" label above the device name in bold
|
|
||||||
- Right: Disconnect button (`bluetooth_disabled` crossed-out icon) and Settings button (tune icon)
|
|
||||||
|
|
||||||
**Body**:
|
|
||||||
- **Connection Card**: Device avatar, device name, device ID, "Connected" chip, and battery chip
|
|
||||||
- **Quick Switch** section: The QuickSwitchBar widget for navigating to Contacts/Channels/Map
|
|
||||||
|
|
||||||
### Disconnection
|
|
||||||
|
|
||||||
- The disconnect button shows a confirmation dialog before disconnecting
|
|
||||||
- If the device disconnects unexpectedly, the app automatically navigates back to the Scanner screen (fires after the current frame completes via a post-frame callback)
|
- If the device disconnects unexpectedly, the app automatically navigates back to the Scanner screen (fires after the current frame completes via a post-frame callback)
|
||||||
- This auto-navigation behavior (`DisconnectNavigationMixin`) is shared across all main screens
|
- This auto-navigation behavior (`DisconnectNavigationMixin`) is shared across all main screens
|
||||||
|
|
||||||
## Theme and Locale
|
## Theme and Locale
|
||||||
|
|
||||||
- **Theme mode** is user-configurable in App Settings (System / Light / Dark) — not locked to system
|
- **Theme mode** is user-configurable in App Settings (System / Light / Dark) — not locked to system
|
||||||
- **Language** can be overridden to one of 15 supported languages, or follow the system locale
|
- **Language** can be overridden to one of 18 supported languages, or follow the system locale
|
||||||
- On web, if a non-Chromium browser is detected, the app shows a `ChromeRequiredScreen` instead of the Scanner (Web Bluetooth requires Chromium)
|
- On web, if a non-Chromium browser is detected, the app shows a `ChromeRequiredScreen` instead of the Scanner (Web Bluetooth requires Chromium)
|
||||||
|
|
||||||
## Full Navigation Graph
|
## Full Navigation Graph
|
||||||
|
|
||||||
```
|
```
|
||||||
ScannerScreen (root, always on stack)
|
ScannerScreen (root, always on stack)
|
||||||
├─ [BLE connect] → push → ContactsScreen
|
├─ [BLE connect] → push → ChannelsScreen
|
||||||
├─ [TCP FAB] → push → TcpScreen
|
├─ [TCP icon button] → push → TcpScreen
|
||||||
│ └─ [TCP connected] → pushReplacement → ContactsScreen
|
│ └─ [TCP connected] → pushReplacement → ChannelsScreen
|
||||||
└─ [USB FAB] → push → UsbScreen
|
└─ [USB icon button] → push → UsbScreen
|
||||||
└─ [USB connected] → pushReplacement → ContactsScreen
|
└─ [USB connected] → pushReplacement → ChannelsScreen
|
||||||
|
|
||||||
ContactsScreen (selected=0)
|
ContactsScreen (selected=0)
|
||||||
├─ [quick-switch 1] → pushReplacement → ChannelsScreen
|
├─ [quick-switch 1] → pushReplacement → ChannelsScreen
|
||||||
@@ -75,9 +60,9 @@ ChannelsScreen (selected=1)
|
|||||||
MapScreen (selected=2)
|
MapScreen (selected=2)
|
||||||
├─ [quick-switch 0] → pushReplacement → ContactsScreen
|
├─ [quick-switch 0] → pushReplacement → ContactsScreen
|
||||||
├─ [quick-switch 1] → pushReplacement → ChannelsScreen
|
├─ [quick-switch 1] → pushReplacement → ChannelsScreen
|
||||||
├─ [radar button] → push → PathTraceMapScreen
|
├─ [radar menu item] → enters in-map path trace mode (push → PathTraceMapScreen after path is built)
|
||||||
├─ [terrain button] → push → LineOfSightMapScreen
|
├─ [terrain menu item] → push → LineOfSightMapScreen
|
||||||
└─ [long-press] → share marker / set location
|
└─ [long-press] → share marker sheet
|
||||||
|
|
||||||
Settings (push from any main screen)
|
Settings (push from any main screen)
|
||||||
└─ [App Settings] → push → AppSettingsScreen
|
└─ [App Settings] → push → AppSettingsScreen
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ MeshCore Open provides both **system notifications** (push-style OS alerts) and
|
|||||||
|
|
||||||
### 3. Advertisement Notifications
|
### 3. Advertisement Notifications
|
||||||
- **Triggered when**: A new node is discovered on the mesh for the first time
|
- **Triggered when**: A new node is discovered on the mesh for the first time
|
||||||
- **Title**: "New [type] discovered" (e.g., "New chat node discovered")
|
- **Title**: "New [type] discovered" (e.g., "New Chat discovered")
|
||||||
- **Body**: Contact's name
|
- **Body**: Contact's name
|
||||||
- **Priority**: Default
|
- **Priority**: Default
|
||||||
- **Android channel**: `adverts`
|
- **Android channel**: `adverts`
|
||||||
@@ -43,7 +43,7 @@ Red numeric badges appear throughout the UI:
|
|||||||
- **Contacts list**: Each contact row shows a red pill badge (e.g., "3") for unread messages
|
- **Contacts list**: Each contact row shows a red pill badge (e.g., "3") for unread messages
|
||||||
- **Channels list**: Each channel row shows an unread badge
|
- **Channels list**: Each channel row shows an unread badge
|
||||||
- **Chat screen subtitle**: Shows unread count inline
|
- **Chat screen subtitle**: Shows unread count inline
|
||||||
- Badges cap at "99+" for display
|
- Badges cap at "9999+" for display
|
||||||
|
|
||||||
### How Unread Counts Work
|
### How Unread Counts Work
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ There is no per-contact muting.
|
|||||||
|
|
||||||
The notification system prevents notification storms:
|
The notification system prevents notification storms:
|
||||||
- **Minimum interval**: 3 seconds between individual notifications
|
- **Minimum interval**: 3 seconds between individual notifications
|
||||||
- **Batch window**: If multiple notifications arrive within 5 seconds, they are combined into a single summary notification on a fourth Android channel (`batch_summary`): "MeshCore Activity — 2 messages, 1 channel message, 3 new nodes". Note: batch summaries are Android-only; on Apple platforms individual notifications are shown
|
- **Batch window**: If multiple notifications arrive within 5 seconds, they are combined into a single summary notification on a fourth Android channel (`batch_summary`). The title is "MeshCore Activity" and the body lists the grouped counts (e.g., "2 messages, 1 channel message, 3 new nodes"). Batch summaries are Android-only; queued notifications that overflow the batch window are silently dropped on other platforms
|
||||||
|
|
||||||
## Notification Clearing
|
## Notification Clearing
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ From the Contacts screen:
|
|||||||
- Password field with show/hide toggle
|
- Password field with show/hide toggle
|
||||||
- "Save password" checkbox (persists for future logins). If a saved password exists, it is pre-filled and the checkbox is pre-checked, making login one-tap
|
- "Save password" checkbox (persists for future logins). If a saved password exists, it is pre-filled and the checkbox is pre-checked, making login one-tap
|
||||||
- Routing mode selector and "Manage Paths" link are available directly in the dialog (configure routing before login)
|
- Routing mode selector and "Manage Paths" link are available directly in the dialog (configure routing before login)
|
||||||
- Auto-retries up to 5 times on timeout, showing progress ("Attempt 2 of 5"). A wrong password stops immediately after the first attempt — only timeouts trigger retries
|
- Auto-retries up to 5 times on timeout, showing progress ("Attempt 2 of 5"). A wrong password (explicit failure response) stops immediately — only timeouts trigger retries
|
||||||
- After 5 failed attempts, further login attempts are blocked
|
- If auto-clock-sync is enabled for this repeater (configured in Repeater Settings), a `clock sync` command is sent automatically on successful login
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -28,15 +28,17 @@ The central management screen showing:
|
|||||||
|
|
||||||
- **Header card**: Repeater name, short public key, path label, GPS coordinates (if known)
|
- **Header card**: Repeater name, short public key, path label, GPS coordinates (if known)
|
||||||
- **Battery chemistry selector**: NMC / LiFePO4 / LiPo (saved per repeater)
|
- **Battery chemistry selector**: NMC / LiFePO4 / LiPo (saved per repeater)
|
||||||
- **Management tool cards** (full-width cards with chevron arrows, not a grid). Title dynamically shows "Repeater Management" or "Room Management" based on contact type:
|
- **Management tool cards** (full-width cards with chevron arrows, not a grid). Title dynamically shows "Repeater Management" or "Room Management" (admin) or "Repeater Guest" / "Room Guest" (guest) based on contact type and login result:
|
||||||
|
|
||||||
| Card | Destination |
|
| Card | Destination | Visibility |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| Status | Repeater Status Screen |
|
| Status | Repeater Status Screen | All users |
|
||||||
| Telemetry | Telemetry Screen |
|
| Telemetry | Telemetry Screen | All users |
|
||||||
| CLI | Repeater CLI Screen |
|
| Neighbors | Neighbors Screen | All users |
|
||||||
| Neighbors | Neighbors Screen |
|
| CLI | Repeater CLI Screen | Admin only |
|
||||||
| Settings | Repeater Settings Screen |
|
| Settings | Repeater Settings Screen | Admin only |
|
||||||
|
|
||||||
|
The battery chemistry selector and CLI/Settings cards are hidden from guest users.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -47,26 +49,28 @@ The central management screen showing:
|
|||||||
Three information cards:
|
Three information cards:
|
||||||
|
|
||||||
**System Information**:
|
**System Information**:
|
||||||
- Battery percentage
|
- Battery percentage and voltage (e.g. "85% / 3.95V"), using the battery chemistry set in the hub screen
|
||||||
- Uptime
|
|
||||||
- Queue length
|
|
||||||
- Error flags
|
|
||||||
- Clock at login time
|
- Clock at login time
|
||||||
|
- Uptime (days/hours/minutes/seconds)
|
||||||
|
- Queue length
|
||||||
|
- Debug flags (error event count)
|
||||||
|
|
||||||
**Radio Statistics**:
|
**Radio Statistics**:
|
||||||
- Last RSSI and SNR
|
- Last RSSI and SNR
|
||||||
- Noise floor
|
- Noise floor
|
||||||
- TX and RX airtime
|
- TX airtime and RX airtime
|
||||||
|
|
||||||
**Packet Statistics**:
|
**Packet Statistics**:
|
||||||
- Packets sent, received, and duplicates
|
- Packets sent and received, each broken down by flood vs. direct
|
||||||
- Broken down by flood vs. direct
|
- Duplicates, broken down by flood vs. direct
|
||||||
|
- Channel utilization (% of uptime used by TX + RX)
|
||||||
|
|
||||||
### Key Interactions
|
### Key Interactions
|
||||||
- Auto-queries the repeater on open; shows a loading spinner until data arrives
|
- Auto-queries the repeater on open; shows a loading spinner until data arrives
|
||||||
- On timeout: red snackbar error. On success: data appears with a green snackbar confirmation
|
- On timeout: red snackbar error. On success: data appears in-place (no extra snackbar)
|
||||||
- Pull-to-refresh or refresh button to re-query
|
- Pull-to-refresh or refresh button in the app bar to re-query
|
||||||
- Routing mode popup and path management dialog in app bar (these controls appear on **all** management sub-screens, not just Status)
|
- Routing mode popup and path management dialog in app bar (these controls appear on **all** management sub-screens, not just Status)
|
||||||
|
- Accepts both binary `RESP_CODE_STATUS_RESPONSE` frames and legacy JSON text responses
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -76,7 +80,7 @@ A terminal-style interface for sending commands directly to the repeater.
|
|||||||
|
|
||||||
### What the User Sees
|
### What the User Sees
|
||||||
|
|
||||||
- **Quick-command bar** (horizontal scroll): Shortcut buttons for common commands (get name, get radio, get tx, neighbors, ver, advert, clock)
|
- **Quick-command bar** (horizontal scroll): Shortcut buttons for 9 common commands (advert, get name, get radio, get tx, discover.neighbors, neighbors, ver, clock, clock sync)
|
||||||
- **Command history list**: Sent commands in primary color, responses in secondary color
|
- **Command history list**: Sent commands in primary color, responses in secondary color
|
||||||
- **Input bar**: Up/down history arrows, monospace text field with `> ` prefix, send button
|
- **Input bar**: Up/down history arrows, monospace text field with `> ` prefix, send button
|
||||||
|
|
||||||
@@ -85,16 +89,20 @@ A terminal-style interface for sending commands directly to the repeater.
|
|||||||
- Type a command and press send (or Enter on desktop)
|
- Type a command and press send (or Enter on desktop)
|
||||||
- Up/down arrows navigate through command history
|
- Up/down arrows navigate through command history
|
||||||
- Quick-command buttons populate and send common commands
|
- Quick-command buttons populate and send common commands
|
||||||
- Bug report icon: Shows raw frame debug info for the next typed command (shows error snackbar if input field is empty)
|
- Overflow menu (three-dot icon): "Debug next command" option shows raw frame debug info for the next typed command (shows error snackbar if input field is empty)
|
||||||
- Help icon: Opens a scrollable reference of all known CLI commands. Tapping any command populates the input field immediately
|
- Help icon: Opens a scrollable reference of all known CLI commands. Tapping any command populates the input field immediately
|
||||||
- Clear icon: Wipes the command/response history
|
- Clear icon: Wipes the command/response history
|
||||||
- Failed/timed-out commands are automatically retried once
|
- Failed/timed-out commands are automatically retried once
|
||||||
|
|
||||||
### Available CLI Commands
|
### Available CLI Commands
|
||||||
|
|
||||||
**General**: `advert`, `reboot`, `clock`, `password`, `ver`, `clear stats`
|
The in-app help reference (help icon) documents all known commands. Categories:
|
||||||
|
|
||||||
**Settings**: `set name`, `set af`, `set tx`, `set repeat`, `set allow.read.only`, `set flood.max`, `set int.thresh`, `set agc.reset.interval`, `set multi.acks`, `set advert.interval`, `set flood.advert.interval`, `set guest.password`, `set lat`, `set lon`, `set radio`, `set rxdelay`, `set txdelay`, `set direct.txdelay`, `set bridge.*`, `set adc.multiplier`, `tempradio`, `setperm`
|
**General**: `advert`, `advert.zerohop`, `reboot`, `clock`, `clock sync`, `password`, `ver`, `clear stats`, `erase`, `poweroff`, `shutdown`, `clkreboot`, `start ota`, `time`, `board`, `discover.neighbors`, `powersaving`, `stats-packets`, `stats-radio`, `stats-core`
|
||||||
|
|
||||||
|
**Get**: `get name`, `get role`, `get public.key`, `get prv.key`, `get repeat`, `get tx`, `get freq`, `get radio`, `get radio.rxgain`, `get af`, `get dutycycle`, `get int.thresh`, `get agc.reset.interval`, `get multi.acks`, `get allow.read.only`, `get advert.interval`, `get flood.advert.interval`, `get guest.password`, `get lat`, `get lon`, `get rxdelay`, `get txdelay`, `get direct.txdelay`, `get flood.max`, `get owner.info`, `get path.hash.mode`, `get loop.detect`, `get acl`, `get bridge.*`, `get adc.multiplier`, `get bootloader.ver`
|
||||||
|
|
||||||
|
**Set**: `set name`, `set af`, `set tx`, `set repeat`, `set allow.read.only`, `set flood.max`, `set int.thresh`, `set agc.reset.interval`, `set multi.acks`, `set advert.interval`, `set flood.advert.interval`, `set guest.password`, `set lat`, `set lon`, `set freq`, `set radio`, `set rxdelay`, `set txdelay`, `set direct.txdelay`, `set radio.rxgain`, `set dutycycle`, `set loop.detect`, `set path.hash.mode`, `set owner.info`, `set prv.key`, `set bridge.*`, `set adc.multiplier`, `tempradio`, `setperm`
|
||||||
|
|
||||||
**Bridge**: `get bridge.type`
|
**Bridge**: `get bridge.type`
|
||||||
|
|
||||||
@@ -102,9 +110,13 @@ A terminal-style interface for sending commands directly to the repeater.
|
|||||||
|
|
||||||
**Neighbors**: `neighbors`, `neighbor.remove`
|
**Neighbors**: `neighbors`, `neighbor.remove`
|
||||||
|
|
||||||
**Region Management**: `region`, `region load/get/put/remove/allowf/denyf/home/save`
|
**Power Management**: `get pwrmgt.support`, `get pwrmgt.source`, `get pwrmgt.bootreason`, `get pwrmgt.bootmv`
|
||||||
|
|
||||||
**GPS**: `gps`, `gps on/off/sync/setloc/advert`
|
**Sensors**: `sensor get {key}`, `sensor set {key} {value}`, `sensor list [start]`
|
||||||
|
|
||||||
|
**GPS Management**: `gps`, `gps {on|off}`, `gps sync`, `gps setloc`, `gps advert`, `gps advert {none|share|prefs}`
|
||||||
|
|
||||||
|
**Region Management**: `region`, `region load`, `region get`, `region put`, `region remove`, `region allowf`, `region denyf`, `region home`, `region save`, `region default`, `region list allowed`, `region list denied`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -147,40 +159,63 @@ A card titled "Repeater's Neighbors - N" listing each neighbor as:
|
|||||||
|
|
||||||
### What the User Sees
|
### What the User Sees
|
||||||
|
|
||||||
Five configuration cards:
|
Nine configuration cards, each with its own per-field refresh button(s):
|
||||||
|
|
||||||
**1. Basic Settings**
|
**1. Basic Settings**
|
||||||
- Name field
|
- Name field
|
||||||
- Admin password field
|
- Admin password field (write-only; always sent when non-empty)
|
||||||
- Guest password field
|
- Guest password field (write-only; always sent when non-empty)
|
||||||
|
|
||||||
**2. Radio Settings**
|
**2. Radio Settings**
|
||||||
- Frequency (MHz)
|
- Frequency (MHz)
|
||||||
- TX Power (dBm)
|
- TX Power (dBm) — has its own independent refresh button
|
||||||
- Bandwidth dropdown (kHz)
|
- Bandwidth dropdown (kHz)
|
||||||
- Spreading Factor (SF5–SF12)
|
- Spreading Factor (SF5–SF12)
|
||||||
- Coding Rate (4/5–4/8)
|
- Coding Rate (4/5–4/8)
|
||||||
|
- RX Gain boost toggle
|
||||||
|
|
||||||
**3. Location Settings**
|
**3. Location Settings**
|
||||||
- Latitude and longitude fields
|
- Latitude and longitude fields, each with an independent refresh button
|
||||||
|
|
||||||
**4. Features**
|
**4. Features**
|
||||||
- Packet forwarding toggle
|
- Packet forwarding toggle (`set repeat`)
|
||||||
- Guest access toggle
|
- Guest access toggle (`set allow.read.only`)
|
||||||
|
- Multi-ACKs toggle (`set multi.acks`)
|
||||||
|
- Auto clock sync after login toggle (local app setting only, not sent to repeater)
|
||||||
|
|
||||||
**5. Advertisement Settings**
|
**5. Network Health**
|
||||||
|
- Loop detection dropdown (off / minimal / moderate / strict; `set loop.detect`)
|
||||||
|
- Duty cycle slider (1–100%; `set dutycycle`)
|
||||||
|
|
||||||
|
**6. Advertisement Settings**
|
||||||
- Local advert interval slider (60–240 minutes) with enable/disable toggle
|
- Local advert interval slider (60–240 minutes) with enable/disable toggle
|
||||||
- Flood advert interval slider (3–168 hours) with enable/disable toggle
|
- Flood advert interval slider (3–168 hours) with enable/disable toggle
|
||||||
|
- Flood max hops slider (0–64; `set flood.max`)
|
||||||
|
|
||||||
**6. Danger Zone** (red-styled card)
|
**7. Owner Info**
|
||||||
- Reboot repeater
|
- Multi-line text field for operator contact info (`set owner.info`); newlines sent as `|`
|
||||||
- Erase filesystem (serial-only warning)
|
|
||||||
|
**8. Actions** (one-tap, no save needed)
|
||||||
|
- Send Advertisement (`advert`)
|
||||||
|
- Send Zero-Hop Advertisement (`advert.zerohop`)
|
||||||
|
- Clock Sync (`clock sync`)
|
||||||
|
|
||||||
|
**9. Advanced** (collapsed by default)
|
||||||
|
- Path hash mode dropdown (0–2; `set path.hash.mode`)
|
||||||
|
- TX delay field (`set txdelay`)
|
||||||
|
- Direct TX delay field (`set direct.txdelay`)
|
||||||
|
- Interference threshold field (`set int.thresh`)
|
||||||
|
- AGC reset interval slider (0–240s in multiples of 4; `set agc.reset.interval`)
|
||||||
|
|
||||||
|
**Danger Zone** (red-styled card)
|
||||||
|
- Reboot repeater (sends `reboot` with confirmation dialog)
|
||||||
|
- Erase filesystem (serial-only; shows a confirmation dialog, then an informational snackbar — no command is sent over the air)
|
||||||
|
|
||||||
### Key Interactions
|
### Key Interactions
|
||||||
- **Settings are NOT auto-fetched on open**. Only name and location are pre-filled from locally cached contact data. You must tap each section's refresh button to fetch live values from the repeater
|
- **Settings are NOT auto-fetched on open**. Name is pre-filled from cached contact data. Each section has its own refresh button to fetch live values from the repeater
|
||||||
- TX Power has its own separate refresh button, independent from the main Radio Settings refresh
|
- TX Power, RX Gain, latitude, longitude, and advanced fields each have independent inline refresh buttons
|
||||||
- Save button appears when changes are detected
|
- Save button in app bar appears when any change is detected; failed commands keep those fields dirty for retry
|
||||||
- Settings are sent sequentially with 200ms delays between commands (fire-and-forget, no per-command acknowledgment wait)
|
- Settings are sent sequentially with 200ms delays between commands; firmware responses are checked and partial failures are reported in a snackbar
|
||||||
- Validation prevents invalid values (e.g., frequency range, LoRa parameter compatibility)
|
- Some changes (e.g. radio frequency) require a reboot; the firmware response triggers an orange "reboot needed" snackbar
|
||||||
- Advertisement interval sliders reset to defaults when re-enabled (local: 60 min, flood: 3 hours)
|
- Advertisement interval sliders reset to defaults when re-enabled (local: 60 min, flood: 3 hours)
|
||||||
- **Erase Filesystem** does NOT send any command over the air — tapping it only shows a snackbar explaining the operation requires physical serial access. It is effectively non-functional when connected wirelessly
|
- **Erase Filesystem** does NOT send any command over the air — tapping it only shows a snackbar explaining the operation requires physical serial access
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Routing Paths
|
||||||
|
|
||||||
|
This page covers how MeshCore Open represents, selects, validates, and stores routing paths in the UI and data layer.
|
||||||
|
|
||||||
|
## Path Routing
|
||||||
|
|
||||||
|
MeshCore supports variable-length multi-byte routing paths so the app can scale from small meshes to very large node sets.
|
||||||
|
|
||||||
|
### Hash Width and Multi-Byte Paths
|
||||||
|
|
||||||
|
The device capability determines the hash width (number of bytes per hop):
|
||||||
|
|
||||||
|
| Width | Max Unique Nodes | Typical Use |
|
||||||
|
|-------|-----------------|-------------|
|
||||||
|
| 1 byte | 256 | Single-byte node IDs |
|
||||||
|
| 2 bytes | 65,536 | Medium meshes |
|
||||||
|
| 3 bytes | 16.7M | Large networks |
|
||||||
|
| 4 bytes | 4.3G | Very large meshes / future-proofing |
|
||||||
|
|
||||||
|
### Device Capability Detection
|
||||||
|
|
||||||
|
On device connection, the app reads the firmware capability to set `pathHashByteWidth`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Read from device info response (offset 81)
|
||||||
|
final modeRaw = firmwareBytes.length >= 82
|
||||||
|
? (firmwareBytes[81] & 0xFF)
|
||||||
|
: 0;
|
||||||
|
final mode = modeRaw.clamp(0, 3);
|
||||||
|
_pathHashByteWidth = mode + 1; // 1, 2, 3, or 4 bytes per hop
|
||||||
|
```
|
||||||
|
|
||||||
|
The connector reads a single-mode byte and clamps to `0..3`, so the supported hop width is `1..4` bytes. UI code also clamps widths when rendering (typically to `1..4`) so 4-byte hops are handled end-to-end in the current codebase.
|
||||||
|
|
||||||
|
### Path Data Structure
|
||||||
|
|
||||||
|
Paths in messages and storage consist of:
|
||||||
|
|
||||||
|
- **`pathLength` (model/storage)**: Hop count (number of hops). Negative values (e.g. `-1`) are used as a flood sentinel.
|
||||||
|
- **On-air `path_len` byte**: A packed byte that encodes hop count + hash width and is decoded into `pathLength` + `pathBytes` when parsing frames.
|
||||||
|
- **`pathBytes`**: Raw bytes of the path (concatenated hop prefixes), grouped by `pathHashByteWidth`.
|
||||||
|
- **`hopCount`**: Derived display value computed from bytes and width: `(byteCount + hashByteWidth - 1) ~/ hashByteWidth`.
|
||||||
|
- **Example**: With `pathHashByteWidth=2`, a 3-hop path has 6 bytes (`pathBytes.length = 6`) and `pathLength = 3`:
|
||||||
|
- `pathBytes = [0xA1, 0xA2, 0xB1, 0xB2, 0xC1, 0xC2]`
|
||||||
|
- Hops: `[0xA1A2]`, `[0xB1B2]`, `[0xC1C2]`
|
||||||
|
|
||||||
|
### Hop Count Calculation
|
||||||
|
|
||||||
|
Convert path byte length to hop count:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
int hopCount = (byteCount + hashByteWidth - 1) ~/ hashByteWidth;
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this consistently when displaying hop counts in UI. Do not treat `pathLength` as a hop count when the path uses multi-byte hop hashes.
|
||||||
|
|
||||||
|
### Path Usage in Different Message Types
|
||||||
|
|
||||||
|
- **Direct messages**: Extract path from decrypted payload to trace sender route.
|
||||||
|
- **Channel messages**: Decrypt hop-by-hop routing chain from payload; the header carries the encoded byte length for the path blob, not the derived hop count.
|
||||||
|
- **Contact storage**: Path length in byte 32, raw path bytes in bytes 33-96, grouped by detected `pathHashByteWidth`.
|
||||||
|
|
||||||
|
### UI Hop Count Display
|
||||||
|
|
||||||
|
⚠️ **Important**: In screens like "Channel Message Path", prefer the actual decoded `pathBytes` hop count over `pathLength` metadata:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Preferred: use actual observed path length
|
||||||
|
final effectiveHopCount = (pathBytes.length + width - 1) ~/ width;
|
||||||
|
|
||||||
|
// Avoid: using encoded byte length as if it were a hop count
|
||||||
|
// pathLength is bytes; converting it twice causes inflated counts
|
||||||
|
```
|
||||||
|
|
||||||
|
Example scenario:
|
||||||
|
- Radio header reports `pathLength: 32` bytes
|
||||||
|
- Decoded path bytes: `[0xAB, 0xCD]` (2 bytes = 1 hop with width=2)
|
||||||
|
- **Display**: "1 hop" (from `pathBytes`), not "32 hops" (which would double-count the encoded length)
|
||||||
|
|
||||||
@@ -28,9 +28,11 @@ The BLE Scanner is the app's home screen, displayed immediately on launch.
|
|||||||
|
|
||||||
**Device List**: When no devices are found, shows a large Bluetooth icon with a prompt. The prompt text is dynamic: "Searching for devices..." while actively scanning, or "Tap Scan to search" when idle. When devices are found, shows a scrollable list of `DeviceTile` widgets.
|
**Device List**: When no devices are found, shows a large Bluetooth icon with a prompt. The prompt text is dynamic: "Searching for devices..." while actively scanning, or "Tap Scan to search" when idle. When devices are found, shows a scrollable list of `DeviceTile` widgets.
|
||||||
|
|
||||||
**Bottom FAB Row**: Up to three floating action buttons:
|
**App Bar Actions**: Icon buttons in the top-right corner of the app bar:
|
||||||
- **USB** button - Opens USB connection screen (Android, Windows, Linux, macOS, Chrome web only)
|
- **USB** icon button - Opens USB connection screen (Android, Windows, Linux, macOS, Chrome web only)
|
||||||
- **TCP/IP** button - Opens TCP connection screen (all non-web platforms)
|
- **TCP/IP** icon button - Opens TCP connection screen (all non-web platforms)
|
||||||
|
|
||||||
|
**Bottom FAB**: A single floating action button:
|
||||||
- **BLE Scan** button - Toggles BLE scanning on/off; shows a spinner when scanning. **Disabled** (greyed out, not tappable) when Bluetooth is off
|
- **BLE Scan** button - Toggles BLE scanning on/off; shows a spinner when scanning. **Disabled** (greyed out, not tappable) when Bluetooth is off
|
||||||
|
|
||||||
### Device Tile
|
### Device Tile
|
||||||
@@ -51,7 +53,7 @@ Note: The weak (-80 to -90 dBm) and poor (< -90 dBm) tiers share the same icon s
|
|||||||
|
|
||||||
### How Scanning Works
|
### How Scanning Works
|
||||||
|
|
||||||
- Filters for devices with names starting with `MeshCore-` or `Whisper-`
|
- Filters for devices advertising the Nordic UART Service UUID (so community forks with non-standard names are still found). Known name prefixes used by stock firmware builds for reference: `MeshCore-`, `Whisper-`, `WisCore-`, `Seeed`, `Lilygo`, `HT-`, `LowMesh_MC_`, `NRF52`
|
||||||
- Uses low-latency scan mode on Android
|
- Uses low-latency scan mode on Android
|
||||||
- Scans for 10 seconds then auto-stops
|
- Scans for 10 seconds then auto-stops
|
||||||
- On iOS/macOS, waits for BLE adapter initialization before starting
|
- On iOS/macOS, waits for BLE adapter initialization before starting
|
||||||
@@ -61,11 +63,11 @@ Note: The weak (-80 to -90 dBm) and poor (< -90 dBm) tiers share the same icon s
|
|||||||
|
|
||||||
Tap a device tile or its Connect button:
|
Tap a device tile or its Connect button:
|
||||||
1. The connector stops scanning and transitions to "connecting"
|
1. The connector stops scanning and transitions to "connecting"
|
||||||
2. Connects to the device with a 15-second timeout
|
2. Connects to the device with a 15-second timeout (6 seconds on Linux)
|
||||||
3. Requests MTU 185 bytes for optimal throughput
|
3. Requests MTU 185 bytes for optimal throughput
|
||||||
4. Discovers BLE services and locates the Nordic UART Service
|
4. Discovers BLE services and locates the Nordic UART Service
|
||||||
5. Subscribes to TX notifications for receiving data
|
5. Subscribes to TX notifications for receiving data
|
||||||
6. On success, automatically navigates to the Contacts screen
|
6. On success, automatically navigates to the Channels screen
|
||||||
7. On failure, shows a red error snackbar
|
7. On failure, shows a red error snackbar
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -74,7 +76,7 @@ Tap a device tile or its Connect button:
|
|||||||
|
|
||||||
### How to Access
|
### How to Access
|
||||||
|
|
||||||
From the Scanner screen, tap the **USB** FAB button.
|
From the Scanner screen, tap the **USB** icon button in the app bar.
|
||||||
|
|
||||||
### What the User Sees
|
### What the User Sees
|
||||||
|
|
||||||
@@ -82,15 +84,15 @@ From the Scanner screen, tap the **USB** FAB button.
|
|||||||
- A list of detected USB serial ports, each showing:
|
- A list of detected USB serial ports, each showing:
|
||||||
- Friendly display name
|
- Friendly display name
|
||||||
- Raw port name (subtitle, only shown when it differs from the display name)
|
- Raw port name (subtitle, only shown when it differs from the display name)
|
||||||
- "Connect" button
|
- Chevron trailing icon (the entire tile is tappable to connect)
|
||||||
- FABs at the bottom to switch to BLE or TCP (these use `pushReplacement`, so back navigation returns to Scanner, not between USB/TCP)
|
- Transport switcher buttons (outlined, not FABs) to switch to BLE or TCP (these use `pushReplacement`, so back navigation returns to Scanner, not between USB/TCP)
|
||||||
|
|
||||||
### Key Interactions
|
### Key Interactions
|
||||||
|
|
||||||
- On desktop (Windows, Linux, macOS): ports are polled every 2 seconds for hot-plug detection (polling pauses while connecting/connected)
|
- On desktop (Windows, Linux, macOS): ports are polled every 2 seconds for hot-plug detection (polling pauses while connecting/connected)
|
||||||
- On mobile: tap the "Scan" FAB to manually refresh
|
- On mobile: tap the "Scan" FAB to manually refresh
|
||||||
- Tap a port or its Connect button to connect
|
- Tap a port tile to connect
|
||||||
- On successful connection, navigates to Contacts screen
|
- On successful connection, navigates to Channels screen
|
||||||
- On connection failure, the port list automatically refreshes
|
- On connection failure, the port list automatically refreshes
|
||||||
- Platform-specific error messages for common USB failures (permission denied, device missing, device detached, device busy, driver missing, port invalid, timeout, and more)
|
- Platform-specific error messages for common USB failures (permission denied, device missing, device detached, device busy, driver missing, port invalid, timeout, and more)
|
||||||
|
|
||||||
@@ -100,7 +102,7 @@ From the Scanner screen, tap the **USB** FAB button.
|
|||||||
|
|
||||||
### How to Access
|
### How to Access
|
||||||
|
|
||||||
From the Scanner screen, tap the **TCP/IP** FAB button.
|
From the Scanner screen, tap the **TCP/IP** icon button in the app bar.
|
||||||
|
|
||||||
### What the User Sees
|
### What the User Sees
|
||||||
|
|
||||||
@@ -108,7 +110,7 @@ From the Scanner screen, tap the **TCP/IP** FAB button.
|
|||||||
- **Host address** text field
|
- **Host address** text field
|
||||||
- **Port number** text field
|
- **Port number** text field
|
||||||
- **Connect** button
|
- **Connect** button
|
||||||
- FABs at the bottom to switch to USB or BLE
|
- Transport switcher buttons (outlined, not FABs) to switch to USB or BLE
|
||||||
|
|
||||||
### Key Interactions
|
### Key Interactions
|
||||||
|
|
||||||
@@ -119,6 +121,6 @@ From the Scanner screen, tap the **TCP/IP** FAB button.
|
|||||||
- Validation errors are shown as red snackbars
|
- Validation errors are shown as red snackbars
|
||||||
- The Connect button shows a spinner and "Connecting..." label while in progress
|
- The Connect button shows a spinner and "Connecting..." label while in progress
|
||||||
- The status bar shows the specific host:port being connected to (e.g., "Connecting to 192.168.1.1:5000")
|
- The status bar shows the specific host:port being connected to (e.g., "Connecting to 192.168.1.1:5000")
|
||||||
- On success, navigates to Contacts screen and saves the host/port to settings
|
- On success, navigates to Channels screen and saves the host/port to settings
|
||||||
- On connection, the status bar shows the active TCP endpoint (e.g., "Connected to 192.168.1.1:5000")
|
- On connection, the status bar shows the active TCP endpoint (e.g., "Connected to 192.168.1.1:5000")
|
||||||
- Error messages for timeout, unsupported platform, and connection failures
|
- Error messages for timeout, unsupported platform, and connection failures
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ Settings are only accessible while a device is connected.
|
|||||||
The settings screen is a scrollable list of cards:
|
The settings screen is a scrollable list of cards:
|
||||||
|
|
||||||
1. [Device Info](#device-info)
|
1. [Device Info](#device-info)
|
||||||
2. [App Settings](#app-settings) (link to sub-screen)
|
2. [Node Settings](#node-settings)
|
||||||
3. [Node Settings](#node-settings)
|
3. [Location](#location)
|
||||||
4. [Actions](#actions)
|
4. [App Settings](#app-settings) (link to sub-screen)
|
||||||
5. [Debug](#debug)
|
5. [Actions](#actions)
|
||||||
6. [Export](#export)
|
6. [Export](#export)
|
||||||
7. [About](#about)
|
7. [Debug](#debug)
|
||||||
|
8. [About](#about)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -40,46 +41,6 @@ Battery shows an alert icon and orange text when at 15% or below. The toggle onl
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## App Settings
|
|
||||||
|
|
||||||
A dedicated sub-screen for app-level preferences (nothing here is sent to the device). All settings persist locally via SharedPreferences.
|
|
||||||
|
|
||||||
### Appearance
|
|
||||||
- **Theme**: System / Light / Dark
|
|
||||||
- **Language**: System default or one of 15 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian)
|
|
||||||
- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages
|
|
||||||
|
|
||||||
### Notifications
|
|
||||||
- **Master enable/disable**: Requests OS permission when enabling
|
|
||||||
- **Message notifications**: New direct message alerts
|
|
||||||
- **Channel message notifications**: New channel message alerts
|
|
||||||
- **Advertisement notifications**: New node discovery alerts
|
|
||||||
|
|
||||||
### Messaging
|
|
||||||
- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail
|
|
||||||
- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off):
|
|
||||||
- Max Route Weight (1–10, default 5, integer steps)
|
|
||||||
- Initial Route Weight (0.5–5.0, default 3.0)
|
|
||||||
- Success Increment (0.1–2.0, default 0.5, 0.1 steps)
|
|
||||||
- Failure Decrement (0.1–2.0, default 0.2, 0.1 steps)
|
|
||||||
- Max Message Retries (2–10, default 5)
|
|
||||||
|
|
||||||
### Battery
|
|
||||||
- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage)
|
|
||||||
|
|
||||||
### Map Display
|
|
||||||
- **Show Repeaters**: Toggle repeater markers on map
|
|
||||||
- **Show Chat Nodes**: Toggle chat node markers
|
|
||||||
- **Show Other Nodes**: Toggle room/sensor markers
|
|
||||||
- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week
|
|
||||||
- **Units**: Metric / Imperial
|
|
||||||
- **Offline Map Cache**: Navigate to tile download screen
|
|
||||||
|
|
||||||
### Debug
|
|
||||||
- **App Debug Logging**: Enable the in-app debug log
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Node Settings
|
## Node Settings
|
||||||
|
|
||||||
These settings are sent directly to the connected device firmware.
|
These settings are sent directly to the connected device firmware.
|
||||||
@@ -91,7 +52,7 @@ These settings are sent directly to the connected device firmware.
|
|||||||
|
|
||||||
### Radio Settings
|
### Radio Settings
|
||||||
Opens a dialog pre-populated with the device's current radio settings. Contains:
|
Opens a dialog pre-populated with the device's current radio settings. Contains:
|
||||||
- **Preset dropdown**: 19 regional presets — selecting a preset immediately fills all fields below. Full list: Australia, Australia (Narrow), Australia SA/WA/QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, Switzerland, USA Arizona, USA/Canada, Vietnam, Off-Grid 433, Off-Grid 869, Off-Grid 918
|
- **Preset dropdown**: Regional presets — selecting a preset immediately fills all fields below. Includes presets for Australia, Australia (Narrow), Australia (Mid), Australia SA WA QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, numerous Russia city presets, Switzerland, USA Arizona, USA/Canada, and Vietnam
|
||||||
- **Frequency** (MHz): Free text, validated 300–2500 MHz
|
- **Frequency** (MHz): Free text, validated 300–2500 MHz
|
||||||
- **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz)
|
- **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz)
|
||||||
- **Spreading Factor**: SF5–SF12
|
- **Spreading Factor**: SF5–SF12
|
||||||
@@ -99,6 +60,13 @@ Opens a dialog pre-populated with the device's current radio settings. Contains:
|
|||||||
- **TX Power** (dBm): Validated 0 to device max (typically 22 dBm)
|
- **TX Power** (dBm): Validated 0 to device max (typically 22 dBm)
|
||||||
- **Client Repeat** toggle: Only shown on firmware v9+; requires frequency to be exactly 433.000, 869.000, or 918.000 MHz (the Off-Grid presets). Save is blocked with a warning if enabled on other frequencies
|
- **Client Repeat** toggle: Only shown on firmware v9+; requires frequency to be exactly 433.000, 869.000, or 918.000 MHz (the Off-Grid presets). Save is blocked with a warning if enabled on other frequencies
|
||||||
|
|
||||||
|
### Companion Radio Stats
|
||||||
|
Opens the RF statistics screen (RSSI, SNR, packet counts) for the paired radio. Only enabled when connected to a device that supports companion radio stats.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Location
|
||||||
|
|
||||||
### Location
|
### Location
|
||||||
Opens a dialog pre-populated with the device's current coordinates (if known):
|
Opens a dialog pre-populated with the device's current coordinates (if known):
|
||||||
- Latitude and longitude fields (decimal, 6 decimal places). If only one field is provided, the other uses the device's current value
|
- Latitude and longitude fields (decimal, 6 decimal places). If only one field is provided, the other uses the device's current value
|
||||||
@@ -115,8 +83,72 @@ Five toggles controlling which node types are auto-added when heard:
|
|||||||
- Auto-add Sensors
|
- Auto-add Sensors
|
||||||
- Overwrite Oldest (when contact list is full)
|
- Overwrite Oldest (when contact list is full)
|
||||||
|
|
||||||
### Privacy Mode
|
### Privacy
|
||||||
Opens a confirmation dialog with three buttons: Cancel, Enable, and Disable. Both states can be set from the same dialog regardless of current state. A snackbar confirms which state was applied. When on, the node stops broadcasting its location in advertisements.
|
Opens a dialog with controls for how the node shares telemetry and location data:
|
||||||
|
- **Advert Location**: Toggle whether the node broadcasts its location in advertisements
|
||||||
|
- **Multi-Ack**: Toggle multi-ack delivery confirmations
|
||||||
|
- **Telemetry Base Mode**: Deny All / Allow by Contact / Allow All
|
||||||
|
- **Telemetry Location Mode**: Deny All / Allow by Contact / Allow All
|
||||||
|
- **Telemetry Environment Mode**: Deny All / Allow by Contact / Allow All
|
||||||
|
|
||||||
|
Settings take effect when saved. A snackbar confirms the update.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## App Settings
|
||||||
|
|
||||||
|
A dedicated sub-screen for app-level preferences (nothing here is sent to the device). All settings persist locally via SharedPreferences.
|
||||||
|
|
||||||
|
### Appearance
|
||||||
|
- **Theme**: System / Light / Dark
|
||||||
|
- **Language**: System default or one of 18 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian, Hungarian, Japanese, Korean)
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
- **Master enable/disable**: Requests OS permission when enabling
|
||||||
|
- **Message notifications**: New direct message alerts
|
||||||
|
- **Channel message notifications**: New channel message alerts
|
||||||
|
- **Advertisement notifications**: New node discovery alerts
|
||||||
|
|
||||||
|
### Messaging
|
||||||
|
- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail
|
||||||
|
- **Jump to Oldest Unread**: When opening a chat, scrolls to the oldest unread message instead of the newest
|
||||||
|
- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off):
|
||||||
|
- Max Route Weight (1–10, default 5, integer steps)
|
||||||
|
- Initial Route Weight (0.5–5.0, default 3.0)
|
||||||
|
- Success Increment (0.1–2.0, default 0.5, 0.1 steps)
|
||||||
|
- Failure Decrement (0.1–2.0, default 0.2, 0.1 steps)
|
||||||
|
- Max Message Retries (2–10, default 5)
|
||||||
|
- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages
|
||||||
|
|
||||||
|
### Battery
|
||||||
|
- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage)
|
||||||
|
|
||||||
|
### Map Display
|
||||||
|
- **Show Repeaters**: Toggle repeater markers on map
|
||||||
|
- **Show Chat Nodes**: Toggle chat node markers
|
||||||
|
- **Show Other Nodes**: Toggle room/sensor markers
|
||||||
|
- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week
|
||||||
|
- **Units**: Metric / Imperial
|
||||||
|
- **Raster Tile Source**: Sets the MAP theme and with that from where to get the map tile data:
|
||||||
|
- OpenStreetMap (Auto/Standard/Dark) is provided by the free OpenStreetMap tile server. (Can only be used for live view or already cached.)
|
||||||
|
- Stamen Terrain / AlidadeSmooth Dark / Outdoors / OSM Bright are [StadiaMaps.com raster tile maps](https://stadiamaps.com/products/maps/interactive-basemaps/) for which you can choose an Hosted Endpoint (Worldwide / Europe hosted) and have to provide the API key to your subscription. (You can cache these maps for offline Map usage.)
|
||||||
|
There will be no account provided by meshcore-open. You will have to get you own subscription. StadiaMaps offers a [free](https://stadiamaps.com/pricing/) subscription to download up to 200'000 Standart Raster Basemap tiles.
|
||||||
|
- **Offline Map Cache**: Navigate to tile download screen
|
||||||
|
|
||||||
|
### Translation
|
||||||
|
Not shown on web. Controls on-device message translation powered by a locally-downloaded ML model:
|
||||||
|
- **Enable Translation**: Translates incoming messages into the selected target language
|
||||||
|
- **Translate Composer**: Translates outgoing messages from the target language back before sending
|
||||||
|
- **Target Language**: Language to translate into (searchable list; defaults to the app language)
|
||||||
|
- **Downloaded Model**: Dropdown to select among already-downloaded translation models
|
||||||
|
- **Preset Model**: Download a curated preset model with one tap
|
||||||
|
- **Custom Model URL**: Enter a URL to download a custom GGUF-format model; shows download progress and a cancel button
|
||||||
|
|
||||||
|
### Cyrillic-to-Latin (Cyr2Lat)
|
||||||
|
Controls character substitution profiles used to render Cyrillic text in Latin characters. A dropdown selects the active profile; Add, Edit, and Delete buttons manage the profile list (the last remaining profile cannot be deleted). Each profile stores a JSON character map.
|
||||||
|
|
||||||
|
### Debug
|
||||||
|
- **App Debug Logging**: Enable the in-app debug log
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -126,10 +158,24 @@ One-tap device operations:
|
|||||||
|
|
||||||
| Action | Description |
|
| Action | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Send Advertisement | Floods the mesh with your node's advertisement |
|
|
||||||
| Sync Time | Sends current Unix timestamp to the device |
|
| Sync Time | Sends current Unix timestamp to the device |
|
||||||
| Refresh Contacts | Re-requests the full contact list |
|
| Refresh Contacts | Re-requests the full contact list |
|
||||||
| Reboot Device | Confirmation dialog → reboots the device (shown in orange) |
|
| Reboot Device | Confirmation dialog → reboots the device (shown in warning color) |
|
||||||
|
| Delete All Paths | Confirmation dialog → clears all stored routing paths (shown in alert color) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Export
|
||||||
|
|
||||||
|
Three GPX export options (not available on web):
|
||||||
|
|
||||||
|
| Option | Exports |
|
||||||
|
|---|---|
|
||||||
|
| Export Repeaters | Repeaters and Rooms with GPS coordinates |
|
||||||
|
| Export Contacts | Chat contacts with GPS coordinates |
|
||||||
|
| Export All | All contacts with GPS coordinates |
|
||||||
|
|
||||||
|
Each creates a `.gpx` file and opens the OS share sheet. Feedback via snackbar for four outcomes: success, no contacts with coordinates, feature not available (web), or error.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -150,20 +196,6 @@ Structured log entries (Info / Warning / Error), with tag, message, and timestam
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Export
|
|
||||||
|
|
||||||
Three GPX export options (not available on web):
|
|
||||||
|
|
||||||
| Option | Exports |
|
|
||||||
|---|---|
|
|
||||||
| Export Repeaters | Repeaters and Rooms with GPS coordinates |
|
|
||||||
| Export Contacts | Chat contacts with GPS coordinates |
|
|
||||||
| Export All | All contacts with GPS coordinates |
|
|
||||||
|
|
||||||
Each creates a `.gpx` file and opens the OS share sheet. Feedback via snackbar for four outcomes: success, no contacts with coordinates, feature not available (web), or error.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
Shows the standard Flutter about dialog with app name, version, and legal notice.
|
Shows the standard Flutter about dialog with app name, version, and legal notice.
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>method</key>
|
||||||
|
<string>app-store-connect</string>
|
||||||
|
<key>teamID</key>
|
||||||
|
<string>X74VJ4UAST</string>
|
||||||
|
<key>signingStyle</key>
|
||||||
|
<string>automatic</string>
|
||||||
|
<key>uploadSymbols</key>
|
||||||
|
<true/>
|
||||||
|
<key>destination</key>
|
||||||
|
<string>export</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -20,7 +20,5 @@
|
|||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
|
||||||
<string>13.0</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
platform :ios, '15.5'
|
platform :ios, '16.4'
|
||||||
|
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
@@ -32,5 +32,8 @@ end
|
|||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
installer.pods_project.targets.each do |target|
|
installer.pods_project.targets.each do |target|
|
||||||
flutter_additional_ios_build_settings(target)
|
flutter_additional_ios_build_settings(target)
|
||||||
|
target.build_configurations.each do |config|
|
||||||
|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.4'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,145 +1,22 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_blue_plus_darwin (0.0.2):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- flutter_foreground_task (0.0.1):
|
- flutter_foreground_task (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_local_notifications (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- GoogleDataTransport (10.1.0):
|
|
||||||
- nanopb (~> 3.30910.0)
|
|
||||||
- PromisesObjC (~> 2.4)
|
|
||||||
- GoogleMLKit/BarcodeScanning (7.0.0):
|
|
||||||
- GoogleMLKit/MLKitCore
|
|
||||||
- MLKitBarcodeScanning (~> 6.0.0)
|
|
||||||
- GoogleMLKit/MLKitCore (7.0.0):
|
|
||||||
- MLKitCommon (~> 12.0.0)
|
|
||||||
- GoogleToolboxForMac/Defines (4.2.1)
|
|
||||||
- GoogleToolboxForMac/Logger (4.2.1):
|
|
||||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
|
||||||
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
|
|
||||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
|
||||||
- GoogleUtilities/Environment (8.1.0):
|
|
||||||
- GoogleUtilities/Privacy
|
|
||||||
- GoogleUtilities/Logger (8.1.0):
|
|
||||||
- GoogleUtilities/Environment
|
|
||||||
- GoogleUtilities/Privacy
|
|
||||||
- GoogleUtilities/Privacy (8.1.0)
|
|
||||||
- GoogleUtilities/UserDefaults (8.1.0):
|
|
||||||
- GoogleUtilities/Logger
|
|
||||||
- GoogleUtilities/Privacy
|
|
||||||
- GTMSessionFetcher/Core (3.5.0)
|
|
||||||
- MLImage (1.0.0-beta6)
|
|
||||||
- MLKitBarcodeScanning (6.0.0):
|
|
||||||
- MLKitCommon (~> 12.0)
|
|
||||||
- MLKitVision (~> 8.0)
|
|
||||||
- MLKitCommon (12.0.0):
|
|
||||||
- GoogleDataTransport (~> 10.0)
|
|
||||||
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
|
||||||
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
|
||||||
- GoogleUtilities/Logger (~> 8.0)
|
|
||||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
|
||||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
|
||||||
- MLKitVision (8.0.0):
|
|
||||||
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
|
||||||
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
|
||||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
|
||||||
- MLImage (= 1.0.0-beta6)
|
|
||||||
- MLKitCommon (~> 12.0)
|
|
||||||
- mobile_scanner (6.0.2):
|
|
||||||
- Flutter
|
|
||||||
- GoogleMLKit/BarcodeScanning (~> 7.0.0)
|
|
||||||
- nanopb (3.30910.0):
|
|
||||||
- nanopb/decode (= 3.30910.0)
|
|
||||||
- nanopb/encode (= 3.30910.0)
|
|
||||||
- nanopb/decode (3.30910.0)
|
|
||||||
- nanopb/encode (3.30910.0)
|
|
||||||
- package_info_plus (0.4.5):
|
|
||||||
- Flutter
|
|
||||||
- PromisesObjC (2.4.0)
|
|
||||||
- shared_preferences_foundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- sqflite_darwin (0.0.4):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- url_launcher_ios (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- wakelock_plus (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
|
|
||||||
- flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`)
|
- flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`)
|
||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
|
||||||
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
|
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
|
||||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
|
||||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
|
||||||
|
|
||||||
SPEC REPOS:
|
|
||||||
trunk:
|
|
||||||
- GoogleDataTransport
|
|
||||||
- GoogleMLKit
|
|
||||||
- GoogleToolboxForMac
|
|
||||||
- GoogleUtilities
|
|
||||||
- GTMSessionFetcher
|
|
||||||
- MLImage
|
|
||||||
- MLKitBarcodeScanning
|
|
||||||
- MLKitCommon
|
|
||||||
- MLKitVision
|
|
||||||
- nanopb
|
|
||||||
- PromisesObjC
|
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_blue_plus_darwin:
|
|
||||||
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
|
|
||||||
flutter_foreground_task:
|
flutter_foreground_task:
|
||||||
:path: ".symlinks/plugins/flutter_foreground_task/ios"
|
:path: ".symlinks/plugins/flutter_foreground_task/ios"
|
||||||
flutter_local_notifications:
|
|
||||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
|
||||||
mobile_scanner:
|
|
||||||
:path: ".symlinks/plugins/mobile_scanner/ios"
|
|
||||||
package_info_plus:
|
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
|
||||||
shared_preferences_foundation:
|
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
|
||||||
sqflite_darwin:
|
|
||||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
|
||||||
url_launcher_ios:
|
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
|
||||||
wakelock_plus:
|
|
||||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
|
|
||||||
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
|
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
|
||||||
flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4
|
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
|
||||||
GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318
|
|
||||||
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
|
|
||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
|
||||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
|
||||||
MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56
|
|
||||||
MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2
|
|
||||||
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
|
|
||||||
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
|
|
||||||
mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036
|
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
|
||||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
|
||||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
|
||||||
|
|
||||||
PODFILE CHECKSUM: 570da2a631486c6bd6496bed1e605e63e2471be5
|
PODFILE CHECKSUM: e42b502c78c33aa1ed9d42eaea8960ce2139504b
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
718BC7DCCFC5C370705C12E5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
718BC7DCCFC5C370705C12E5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
@@ -67,6 +69,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||||
9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */,
|
9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -85,6 +88,7 @@
|
|||||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
@@ -179,13 +183,16 @@
|
|||||||
97C146EC1CF9000F007C117D /* Resources */,
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */,
|
F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = Runner;
|
name = Runner;
|
||||||
|
packageProductDependencies = (
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
);
|
||||||
productName = Runner;
|
productName = Runner;
|
||||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
@@ -219,6 +226,9 @@
|
|||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = 97C146E51CF9000F007C117D;
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
|
packageReferences = (
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
|
||||||
|
);
|
||||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
@@ -282,23 +292,6 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
};
|
};
|
||||||
B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputFileListPaths = (
|
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
|
||||||
);
|
|
||||||
name = "[CP] Copy Pods Resources";
|
|
||||||
outputFileListPaths = (
|
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
|
||||||
DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */ = {
|
DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -321,6 +314,22 @@
|
|||||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
|
F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}",
|
||||||
|
);
|
||||||
|
name = "Fix Native Asset Minimum OS";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "set -e\nFRAMEWORKS_DIR=\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nMIN_OS=\"${IPHONEOS_DEPLOYMENT_TARGET}\"\nif [ ! -d \"$FRAMEWORKS_DIR\" ] || [ -z \"$MIN_OS\" ]; then\n exit 0\nfi\nfind \"$FRAMEWORKS_DIR\" -maxdepth 2 -name Info.plist | while read -r plist; do\n bundle_id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \"$plist\" 2>/dev/null || true)\n case \"$bundle_id\" in\n io.flutter.flutter.native-assets.*)\n /usr/libexec/PlistBuddy -c \"Set :MinimumOSVersion $MIN_OS\" \"$plist\" 2>/dev/null || \\\n /usr/libexec/PlistBuddy -c \"Add :MinimumOSVersion string $MIN_OS\" \"$plist\"\n ;;\n esac\ndone\n";
|
||||||
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@@ -414,7 +423,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
@@ -429,7 +438,9 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = X74VJ4UAST;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -540,7 +551,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -591,7 +602,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = iphoneos;
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
@@ -608,7 +619,9 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = X74VJ4UAST;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -630,7 +643,9 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = X74VJ4UAST;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -679,6 +694,20 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
|
||||||
|
isa = XCLocalSwiftPackageReference;
|
||||||
|
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,24 @@
|
|||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES">
|
||||||
|
<PreActions>
|
||||||
|
<ExecutionAction
|
||||||
|
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||||
|
<ActionContent
|
||||||
|
title = "Run Prepare Flutter Framework Script"
|
||||||
|
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||||
|
<EnvironmentBuildable>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</EnvironmentBuildable>
|
||||||
|
</ActionContent>
|
||||||
|
</ExecutionAction>
|
||||||
|
</PreActions>
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "YES"
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ import Flutter
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
@@ -22,8 +24,48 @@
|
|||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>http</string>
|
||||||
|
<string>https</string>
|
||||||
|
</array>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||||
|
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
|
||||||
|
<key>NSBluetoothPeripheralUsageDescription</key>
|
||||||
|
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>This app uses the camera to scan QR codes for joining communities.</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>This app lets you pick a photo to compress and send over the mesh.</string>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneClassName</key>
|
||||||
|
<string>UIWindowScene</string>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>flutter</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>FlutterSceneDelegate</string>
|
||||||
|
<key>UISceneStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>bluetooth-central</string>
|
||||||
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
@@ -41,24 +83,5 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
|
||||||
<true/>
|
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
|
||||||
<true/>
|
|
||||||
<key>UIBackgroundModes</key>
|
|
||||||
<array>
|
|
||||||
<string>bluetooth-central</string>
|
|
||||||
</array>
|
|
||||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
|
||||||
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
|
|
||||||
<key>NSBluetoothPeripheralUsageDescription</key>
|
|
||||||
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
|
|
||||||
<key>NSCameraUsageDescription</key>
|
|
||||||
<string>This app uses the camera to scan QR codes for joining communities.</string>
|
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
|
||||||
<array>
|
|
||||||
<string>http</string>
|
|
||||||
<string>https</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class MeshCoreUsbManager {
|
|||||||
String? get activePortKey => _activePortKey;
|
String? get activePortKey => _activePortKey;
|
||||||
String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey;
|
String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey;
|
||||||
bool get isConnected => _service.isConnected;
|
bool get isConnected => _service.isConnected;
|
||||||
|
Object? get lastError => _service.lastError;
|
||||||
Stream<Uint8List> get frameStream => _service.frameStream;
|
Stream<Uint8List> get frameStream => _service.frameStream;
|
||||||
|
|
||||||
// --- Configuration ---
|
// --- Configuration ---
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:crypto/crypto.dart' as crypto;
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
// Buffer Reader - sequential binary data reader with pointer tracking
|
// Buffer Reader - sequential binary data reader with pointer tracking
|
||||||
@@ -202,13 +203,17 @@ const int cmdGetChannel = 31;
|
|||||||
const int cmdSetChannel = 32;
|
const int cmdSetChannel = 32;
|
||||||
const int cmdSendTracePath = 36;
|
const int cmdSendTracePath = 36;
|
||||||
const int cmdSetOtherParams = 38;
|
const int cmdSetOtherParams = 38;
|
||||||
const int cmdSendAnonReq = 57;
|
|
||||||
const int cmdSendTelemetryReq = 39;
|
const int cmdSendTelemetryReq = 39;
|
||||||
const int cmdGetCustomVar = 40;
|
const int cmdGetCustomVar = 40;
|
||||||
const int cmdSetCustomVar = 41;
|
const int cmdSetCustomVar = 41;
|
||||||
const int cmdSendBinaryReq = 50;
|
const int cmdSendBinaryReq = 50;
|
||||||
|
const int cmdSetFloodScope = 54;
|
||||||
|
const int cmdSendControlData = 55;
|
||||||
|
const int cmdGetStats = 56;
|
||||||
|
const int cmdSendAnonReq = 57;
|
||||||
const int cmdSetAutoAddConfig = 58;
|
const int cmdSetAutoAddConfig = 58;
|
||||||
const int cmdGetAutoAddConfig = 59;
|
const int cmdGetAutoAddConfig = 59;
|
||||||
|
const int cmdSetPathHashMode = 61;
|
||||||
|
|
||||||
// Text message types
|
// Text message types
|
||||||
const int txtTypePlain = 0;
|
const int txtTypePlain = 0;
|
||||||
@@ -222,6 +227,18 @@ const int reqTypeGetTelemetry = 0x03;
|
|||||||
const int reqTypeGetAccessList = 0x05;
|
const int reqTypeGetAccessList = 0x05;
|
||||||
const int reqTypeGetNeighbors = 0x06;
|
const int reqTypeGetNeighbors = 0x06;
|
||||||
|
|
||||||
|
Uint8List buildTelemetryBinaryPayload() {
|
||||||
|
// Room servers/repeaters read byte 1 as an inverse telemetry permission mask.
|
||||||
|
// Zero means "request every telemetry field allowed for this contact".
|
||||||
|
return Uint8List.fromList([reqTypeGetTelemetry, 0x00, 0x00, 0x00, 0x00]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int anonReqTypeRegions = 0x01;
|
||||||
|
|
||||||
|
// Control data sub-types used by MeshCore discovery packets.
|
||||||
|
const int controlSubtypeDiscoverReq = 0x08;
|
||||||
|
const int controlSubtypeDiscoverResp = 0x09;
|
||||||
|
|
||||||
// Repeater response codes
|
// Repeater response codes
|
||||||
const int respServerLoginOk = 0;
|
const int respServerLoginOk = 0;
|
||||||
|
|
||||||
@@ -245,6 +262,11 @@ const int respCodeChannelMsgRecvV3 = 17;
|
|||||||
const int respCodeChannelInfo = 18;
|
const int respCodeChannelInfo = 18;
|
||||||
const int respCodeCustomVars = 21;
|
const int respCodeCustomVars = 21;
|
||||||
const int respCodeAutoAddConfig = 25;
|
const int respCodeAutoAddConfig = 25;
|
||||||
|
const int respCodeStats = 24;
|
||||||
|
|
||||||
|
const int statsTypeCore = 0;
|
||||||
|
const int statsTypeRadio = 1;
|
||||||
|
const int statsTypePackets = 2;
|
||||||
|
|
||||||
// Push codes (async from device)
|
// Push codes (async from device)
|
||||||
const int pushCodeAdvert = 0x80;
|
const int pushCodeAdvert = 0x80;
|
||||||
@@ -259,6 +281,7 @@ const int pushCodeTraceData = 0x89;
|
|||||||
const int pushCodeNewAdvert = 0x8A;
|
const int pushCodeNewAdvert = 0x8A;
|
||||||
const int pushCodeTelemetryResponse = 0x8B;
|
const int pushCodeTelemetryResponse = 0x8B;
|
||||||
const int pushCodeBinaryResponse = 0x8C;
|
const int pushCodeBinaryResponse = 0x8C;
|
||||||
|
const int pushCodeControlData = 0x8E;
|
||||||
|
|
||||||
// Contact/advertisement types
|
// Contact/advertisement types
|
||||||
const int advTypeChat = 1;
|
const int advTypeChat = 1;
|
||||||
@@ -313,7 +336,7 @@ const int maxPathSize = 64;
|
|||||||
const int pathHashSize = 1;
|
const int pathHashSize = 1;
|
||||||
const int maxNameSize = 32;
|
const int maxNameSize = 32;
|
||||||
const int maxFrameSize = 172;
|
const int maxFrameSize = 172;
|
||||||
const int appProtocolVersion = 3;
|
const int appProtocolVersion = 4;
|
||||||
// Matches firmware MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE).
|
// Matches firmware MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE).
|
||||||
const int maxTextPayloadBytes = 160;
|
const int maxTextPayloadBytes = 160;
|
||||||
const int _sendTextMsgOverheadBytes =
|
const int _sendTextMsgOverheadBytes =
|
||||||
@@ -444,8 +467,13 @@ String pubKeyToHex(Uint8List pubKey) {
|
|||||||
|
|
||||||
// Helper to convert hex string to public key
|
// Helper to convert hex string to public key
|
||||||
Uint8List hexToPubKey(String hex) {
|
Uint8List hexToPubKey(String hex) {
|
||||||
|
if (hex.length != pubKeySize * 2) {
|
||||||
|
throw FormatException(
|
||||||
|
'Public key hex must be ${pubKeySize * 2} chars, got ${hex.length}',
|
||||||
|
);
|
||||||
|
}
|
||||||
final result = Uint8List(pubKeySize);
|
final result = Uint8List(pubKeySize);
|
||||||
for (int i = 0; i < pubKeySize && i * 2 + 1 < hex.length; i++) {
|
for (int i = 0; i < pubKeySize; i++) {
|
||||||
result[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16);
|
result[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -554,6 +582,17 @@ Uint8List buildGetBattAndStorageFrame() {
|
|||||||
return Uint8List.fromList([cmdGetBattAndStorage]);
|
return Uint8List.fromList([cmdGetBattAndStorage]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Companion radio stats: [56][statsType] where statsType is statsTypeCore/Radio/Packets.
|
||||||
|
Uint8List buildGetStatsFrame(int statsType) {
|
||||||
|
return Uint8List.fromList([cmdGetStats, statsType & 0xFF]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Path hash width on air: [61][0][mode], mode 0..3 → (mode+1) bytes per hop hash.
|
||||||
|
Uint8List buildSetPathHashModeFrame(int mode) {
|
||||||
|
final m = mode.clamp(0, 3).toInt();
|
||||||
|
return Uint8List.fromList([cmdSetPathHashMode, 0, m]);
|
||||||
|
}
|
||||||
|
|
||||||
// Build CMD_SET_DEVICE_TIME frame
|
// Build CMD_SET_DEVICE_TIME frame
|
||||||
Uint8List buildSetDeviceTimeFrame(int timestamp) {
|
Uint8List buildSetDeviceTimeFrame(int timestamp) {
|
||||||
final writer = BufferWriter();
|
final writer = BufferWriter();
|
||||||
@@ -702,26 +741,20 @@ Uint8List buildUpdateContactPathFrame(
|
|||||||
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||||
writer.writeUInt32LE(timestamp);
|
writer.writeUInt32LE(timestamp);
|
||||||
|
|
||||||
if ((lat == null || lon == null) && lastModified != null) {
|
// Optional [Lat x4, Lon x4][timestamp x4] tail per the doc comment above.
|
||||||
// If lat/lon not provided, write zeros
|
// Emit 8 bytes of position (zero-filled when only lastModified is provided)
|
||||||
writer.writeInt32LE(0);
|
// followed by an optional 4-byte timestamp. Earlier code emitted the
|
||||||
writer.writeInt32LE(0);
|
// position block twice, which corrupted the tail and caused the firmware
|
||||||
} else {
|
// to parse the second lat as the timestamp. See #427.
|
||||||
// Latitude and Longitude are expected in degrees, convert to int by multiplying by 1e6
|
final hasLocation = lat != null && lon != null;
|
||||||
// Latitude
|
if (hasLocation || lastModified != null) {
|
||||||
final latitude = lat ?? 0.0;
|
writer.writeInt32LE(hasLocation ? (lat * 1e6).round() : 0);
|
||||||
writer.writeInt32LE((latitude * 1e6).round());
|
writer.writeInt32LE(hasLocation ? (lon * 1e6).round() : 0);
|
||||||
|
|
||||||
// Longitude
|
|
||||||
final longitude = lon ?? 0.0;
|
|
||||||
writer.writeInt32LE((longitude * 1e6).round());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastModified != null) {
|
if (lastModified != null) {
|
||||||
// Last modified
|
|
||||||
final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
|
final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
|
||||||
writer.writeUInt32LE(lastModifiedTimestamp);
|
writer.writeUInt32LE(lastModifiedTimestamp);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
@@ -843,6 +876,67 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
|
|||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uint8List buildSendControlDataFrame(Uint8List payload) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdSendControlData);
|
||||||
|
writer.writeBytes(payload);
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List buildDiscoveryRequestPayload(
|
||||||
|
int tag, {
|
||||||
|
bool prefixOnly = false,
|
||||||
|
int typeMask = 1 << advTypeRepeater,
|
||||||
|
}) {
|
||||||
|
final writer = BufferWriter();
|
||||||
|
// The high bit must be set for CMD_SEND_CONTROL_DATA; DISCOVER_REQ uses
|
||||||
|
// subtype 0x8, with the low bit selecting short/full public keys in replies.
|
||||||
|
writer.writeByte(
|
||||||
|
(controlSubtypeDiscoverReq << 4) | (prefixOnly ? 0x01 : 0x00),
|
||||||
|
);
|
||||||
|
writer.writeByte(typeMask);
|
||||||
|
writer.writeUInt32LE(tag);
|
||||||
|
writer.writeUInt32LE(0); // since=0 asks nearby nodes for any recent advert.
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List _reversePathByHop(Uint8List path, int pathHashWidth) {
|
||||||
|
if (path.isEmpty) return Uint8List(0);
|
||||||
|
final width = pathHashWidth.clamp(1, 4).toInt();
|
||||||
|
if (path.length % width != 0) {
|
||||||
|
return Uint8List.fromList(path.reversed.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
final reversed = Uint8List(path.length);
|
||||||
|
final hops = path.length ~/ width;
|
||||||
|
for (var i = 0; i < hops; i++) {
|
||||||
|
final from = (hops - 1 - i) * width;
|
||||||
|
reversed.setRange(i * width, (i + 1) * width, path, from);
|
||||||
|
}
|
||||||
|
return reversed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build CMD_SEND_ANON_REQ frame.
|
||||||
|
// Payload format for regions: [anon_req_type][reply_path_len][reply_path...].
|
||||||
|
Uint8List buildSendAnonReqFrame(
|
||||||
|
Uint8List repeaterPubKey, {
|
||||||
|
required int requestType,
|
||||||
|
Uint8List? replyPath,
|
||||||
|
int replyHopCount = 0,
|
||||||
|
int pathHashWidth = pathHashSize,
|
||||||
|
}) {
|
||||||
|
final width = pathHashWidth.clamp(1, 4).toInt();
|
||||||
|
final path = replyPath ?? Uint8List(0);
|
||||||
|
final encodedPathLen = ((width - 1) << 6) | (replyHopCount & 0x3F);
|
||||||
|
final writer = BufferWriter();
|
||||||
|
writer.writeByte(cmdSendAnonReq);
|
||||||
|
writer.writeBytes(repeaterPubKey);
|
||||||
|
writer.writeByte(requestType);
|
||||||
|
writer.writeByte(encodedPathLen);
|
||||||
|
writer.writeBytes(_reversePathByHop(path, width));
|
||||||
|
return writer.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
//Build a trace request frame
|
//Build a trace request frame
|
||||||
//[cmd][tag x4][auth x4][flag][payload]
|
//[cmd][tag x4][auth x4][flag][payload]
|
||||||
Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) {
|
Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) {
|
||||||
@@ -933,7 +1027,22 @@ Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
|
|||||||
writer.writeBytes(Uint8List(3)); // reserved bytes
|
writer.writeBytes(Uint8List(3)); // reserved bytes
|
||||||
writer.writeBytes(pubKey);
|
writer.writeBytes(pubKey);
|
||||||
} else {
|
} else {
|
||||||
writer.writeBytes(Uint8List(4)); // reserved bytes
|
writer.writeBytes(Uint8List(3)); // reserved bytes
|
||||||
}
|
}
|
||||||
return writer.toBytes();
|
return writer.toBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Build CMD_SET_FLOOD_SCOPE
|
||||||
|
// Format: [cmd][scope]
|
||||||
|
Uint8List buildSetFloodScopeFrame(String region) {
|
||||||
|
if (region == '') {
|
||||||
|
// reset scope
|
||||||
|
return Uint8List.fromList([cmdSetFloodScope, 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
final name = region.startsWith('#') ? region : '#$region';
|
||||||
|
final hash = crypto.sha256.convert(utf8.encode(name)).bytes;
|
||||||
|
final scope = Uint8List.fromList(hash.sublist(0, 16));
|
||||||
|
|
||||||
|
return Uint8List.fromList([cmdSetFloodScope, 0, ...scope]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
class MeshCoreUuids {
|
||||||
|
static const String service = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
|
||||||
|
static const String rxCharacteristic = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
|
||||||
|
static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
|
||||||
|
|
||||||
|
/// Known advertised-name prefixes used by stock MeshCore firmware builds.
|
||||||
|
/// Discovery no longer filters on these (it filters on the [service] UUID so
|
||||||
|
/// that community forks with custom names are still found); kept for
|
||||||
|
/// reference and possible future display heuristics.
|
||||||
|
static const List<String> deviceNamePrefixes = [
|
||||||
|
"MeshCore-",
|
||||||
|
"Whisper-",
|
||||||
|
"WisCore-",
|
||||||
|
"Seeed",
|
||||||
|
"Lilygo",
|
||||||
|
"HT-",
|
||||||
|
"LowMesh_MC_",
|
||||||
|
"NRF52",
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -96,6 +96,34 @@ class CayenneLpp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case lppDigitalInput:
|
||||||
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
|
'type': type,
|
||||||
|
'value': buffer.readUInt8(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case lppDigitalOutput:
|
||||||
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
|
'type': type,
|
||||||
|
'value': buffer.readUInt8(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case lppAnalogInput:
|
||||||
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
|
'type': type,
|
||||||
|
'value': buffer.readInt16BE() / 100,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case lppAnalogOutput:
|
||||||
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
|
'type': type,
|
||||||
|
'value': buffer.readInt16BE() / 100,
|
||||||
|
});
|
||||||
|
break;
|
||||||
case lppGenericSensor:
|
case lppGenericSensor:
|
||||||
telemetry.add({
|
telemetry.add({
|
||||||
'channel': channel,
|
'channel': channel,
|
||||||
@@ -131,6 +159,17 @@ class CayenneLpp {
|
|||||||
'value': buffer.readUInt8() / 2,
|
'value': buffer.readUInt8() / 2,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case lppAccelerometer:
|
||||||
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
|
'type': type,
|
||||||
|
'value': {
|
||||||
|
'x': buffer.readInt16BE() / 1000,
|
||||||
|
'y': buffer.readInt16BE() / 1000,
|
||||||
|
'z': buffer.readInt16BE() / 1000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
case lppBarometricPressure:
|
case lppBarometricPressure:
|
||||||
telemetry.add({
|
telemetry.add({
|
||||||
'channel': channel,
|
'channel': channel,
|
||||||
@@ -138,6 +177,13 @@ class CayenneLpp {
|
|||||||
'value': buffer.readUInt16BE() / 10,
|
'value': buffer.readUInt16BE() / 10,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case lppAltitude:
|
||||||
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
|
'type': type,
|
||||||
|
'value': buffer.readInt16BE(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
case lppVoltage:
|
case lppVoltage:
|
||||||
telemetry.add({
|
telemetry.add({
|
||||||
'channel': channel,
|
'channel': channel,
|
||||||
@@ -152,6 +198,13 @@ class CayenneLpp {
|
|||||||
'value': buffer.readInt16BE() / 1000,
|
'value': buffer.readInt16BE() / 1000,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case lppFrequency:
|
||||||
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
|
'type': type,
|
||||||
|
'value': buffer.readUInt32BE(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
case lppPercentage:
|
case lppPercentage:
|
||||||
telemetry.add({
|
telemetry.add({
|
||||||
'channel': channel,
|
'channel': channel,
|
||||||
@@ -173,6 +226,56 @@ class CayenneLpp {
|
|||||||
'value': buffer.readUInt16BE(),
|
'value': buffer.readUInt16BE(),
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case lppDistance:
|
||||||
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
|
'type': type,
|
||||||
|
'value': buffer.readUInt32BE() / 1000,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case lppEnergy:
|
||||||
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
|
'type': type,
|
||||||
|
'value': buffer.readUInt32BE() / 1000,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case lppDirection:
|
||||||
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
|
'type': type,
|
||||||
|
'value': buffer.readUInt16BE(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case lppUnixTime:
|
||||||
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
|
'type': type,
|
||||||
|
'value': buffer.readUInt32BE(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case lppGyrometer:
|
||||||
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
|
'type': type,
|
||||||
|
'value': {
|
||||||
|
'x': buffer.readInt16BE() / 100,
|
||||||
|
'y': buffer.readInt16BE() / 100,
|
||||||
|
'z': buffer.readInt16BE() / 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case lppColour:
|
||||||
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
|
'type': type,
|
||||||
|
'value': {
|
||||||
|
'red': buffer.readUInt8(),
|
||||||
|
'green': buffer.readUInt8(),
|
||||||
|
'blue': buffer.readUInt8(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
case lppGps:
|
case lppGps:
|
||||||
telemetry.add({
|
telemetry.add({
|
||||||
'channel': channel,
|
'channel': channel,
|
||||||
@@ -184,6 +287,24 @@ class CayenneLpp {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case lppSwitch:
|
||||||
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
|
'type': type,
|
||||||
|
'value': buffer.readUInt8(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case lppPolyline:
|
||||||
|
final size = buffer.readUInt8();
|
||||||
|
telemetry.add({
|
||||||
|
'channel': channel,
|
||||||
|
'type': type,
|
||||||
|
'value': {
|
||||||
|
'size': size,
|
||||||
|
'data': _bytesToHex(_readPolylinePayload(buffer, size)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return telemetry;
|
return telemetry;
|
||||||
}
|
}
|
||||||
@@ -216,6 +337,19 @@ class CayenneLpp {
|
|||||||
);
|
);
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case lppDigitalInput:
|
||||||
|
channelData['values']['digitalInput'] = buffer.readUInt8();
|
||||||
|
break;
|
||||||
|
case lppDigitalOutput:
|
||||||
|
channelData['values']['digitalOutput'] = buffer.readUInt8();
|
||||||
|
break;
|
||||||
|
case lppAnalogInput:
|
||||||
|
channelData['values']['analogInput'] = buffer.readInt16BE() / 100.0;
|
||||||
|
break;
|
||||||
|
case lppAnalogOutput:
|
||||||
|
channelData['values']['analogOutput'] =
|
||||||
|
buffer.readInt16BE() / 100.0;
|
||||||
|
break;
|
||||||
case lppGenericSensor:
|
case lppGenericSensor:
|
||||||
channelData['values']['generic'] = buffer.readUInt32BE();
|
channelData['values']['generic'] = buffer.readUInt32BE();
|
||||||
break;
|
break;
|
||||||
@@ -231,15 +365,29 @@ class CayenneLpp {
|
|||||||
case lppRelativeHumidity:
|
case lppRelativeHumidity:
|
||||||
channelData['values']['humidity'] = buffer.readUInt8() / 2.0;
|
channelData['values']['humidity'] = buffer.readUInt8() / 2.0;
|
||||||
break;
|
break;
|
||||||
|
case lppAccelerometer:
|
||||||
|
channelData['values']['accelerometer'] = {
|
||||||
|
'x': buffer.readInt16BE() / 1000.0,
|
||||||
|
'y': buffer.readInt16BE() / 1000.0,
|
||||||
|
'z': buffer.readInt16BE() / 1000.0,
|
||||||
|
};
|
||||||
|
break;
|
||||||
case lppBarometricPressure:
|
case lppBarometricPressure:
|
||||||
channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0;
|
channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0;
|
||||||
break;
|
break;
|
||||||
|
case lppAltitude:
|
||||||
|
// MeshCore encodes standalone barometric altitude as LPP type 121.
|
||||||
|
channelData['values']['altitude'] = buffer.readInt16BE();
|
||||||
|
break;
|
||||||
case lppVoltage:
|
case lppVoltage:
|
||||||
channelData['values']['voltage'] = buffer.readInt16BE() / 100.0;
|
channelData['values']['voltage'] = buffer.readInt16BE() / 100.0;
|
||||||
break;
|
break;
|
||||||
case lppCurrent:
|
case lppCurrent:
|
||||||
channelData['values']['current'] = buffer.readInt16BE() / 1000.0;
|
channelData['values']['current'] = buffer.readInt16BE() / 1000.0;
|
||||||
break;
|
break;
|
||||||
|
case lppFrequency:
|
||||||
|
channelData['values']['frequency'] = buffer.readUInt32BE();
|
||||||
|
break;
|
||||||
case lppPercentage:
|
case lppPercentage:
|
||||||
channelData['values']['percentage'] = buffer.readUInt8();
|
channelData['values']['percentage'] = buffer.readUInt8();
|
||||||
break;
|
break;
|
||||||
@@ -249,6 +397,32 @@ class CayenneLpp {
|
|||||||
case lppPower:
|
case lppPower:
|
||||||
channelData['values']['power'] = buffer.readUInt16BE();
|
channelData['values']['power'] = buffer.readUInt16BE();
|
||||||
break;
|
break;
|
||||||
|
case lppDistance:
|
||||||
|
channelData['values']['distance'] = buffer.readUInt32BE() / 1000.0;
|
||||||
|
break;
|
||||||
|
case lppEnergy:
|
||||||
|
channelData['values']['energy'] = buffer.readUInt32BE() / 1000.0;
|
||||||
|
break;
|
||||||
|
case lppDirection:
|
||||||
|
channelData['values']['direction'] = buffer.readUInt16BE();
|
||||||
|
break;
|
||||||
|
case lppUnixTime:
|
||||||
|
channelData['values']['time'] = buffer.readUInt32BE();
|
||||||
|
break;
|
||||||
|
case lppGyrometer:
|
||||||
|
channelData['values']['gyrometer'] = {
|
||||||
|
'x': buffer.readInt16BE() / 100.0,
|
||||||
|
'y': buffer.readInt16BE() / 100.0,
|
||||||
|
'z': buffer.readInt16BE() / 100.0,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case lppColour:
|
||||||
|
channelData['values']['colour'] = {
|
||||||
|
'red': buffer.readUInt8(),
|
||||||
|
'green': buffer.readUInt8(),
|
||||||
|
'blue': buffer.readUInt8(),
|
||||||
|
};
|
||||||
|
break;
|
||||||
case lppGps:
|
case lppGps:
|
||||||
channelData['values']['gps'] = {
|
channelData['values']['gps'] = {
|
||||||
'latitude': buffer.readInt24BE() / 10000.0,
|
'latitude': buffer.readInt24BE() / 10000.0,
|
||||||
@@ -256,22 +430,48 @@ class CayenneLpp {
|
|||||||
'altitude': buffer.readInt24BE() / 100.0,
|
'altitude': buffer.readInt24BE() / 100.0,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
// Add more types as needed...
|
case lppSwitch:
|
||||||
|
channelData['values']['switch'] = buffer.readUInt8() != 0;
|
||||||
|
break;
|
||||||
|
case lppPolyline:
|
||||||
|
final size = buffer.readUInt8();
|
||||||
|
channelData['values']['polyline'] = {
|
||||||
|
'size': size,
|
||||||
|
'data': _bytesToHex(_readPolylinePayload(buffer, size)),
|
||||||
|
};
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
//Stopped parsing to avoid misalignment
|
// Stop parsing to avoid losing alignment on an unknown LPP type.
|
||||||
return channels.values.toList();
|
return _sortedChannelValues(channels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
|
return _sortedChannelValues(channels);
|
||||||
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
|
|
||||||
return channelsOut;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Handle parsing errors, possibly due to malformed data
|
// Handle parsing errors, possibly due to malformed data
|
||||||
appLogger.error('Error parsing Cayenne LPP data: $e');
|
appLogger.error('Error parsing Cayenne LPP data: $e');
|
||||||
return <
|
// Preserve any fields parsed before the malformed value.
|
||||||
Map<String, dynamic>
|
return _sortedChannelValues(channels);
|
||||||
>[]; // Return an empty list on error to avoid crashing the app
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Uint8List _readPolylinePayload(BufferReader buffer, int size) {
|
||||||
|
final declaredPayloadSize = size > 0 ? size - 1 : 0;
|
||||||
|
final availablePayloadSize = declaredPayloadSize <= buffer.remaining
|
||||||
|
? declaredPayloadSize
|
||||||
|
: buffer.remaining;
|
||||||
|
return buffer.readBytes(availablePayloadSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Map<String, dynamic>> _sortedChannelValues(
|
||||||
|
Map<int, Map<String, dynamic>> channels,
|
||||||
|
) {
|
||||||
|
final channelsOut = channels.values.toList();
|
||||||
|
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
|
||||||
|
return channelsOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _bytesToHex(Uint8List bytes) {
|
||||||
|
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,25 @@ class ChatScrollController extends ScrollController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Jumps toward an off-screen message so that lazy ListView.builder builds
|
||||||
|
/// items near it. Only visible + cacheExtent items have real heights, so we
|
||||||
|
/// use proportion of maxScrollExtent (itself an estimate from built items'
|
||||||
|
/// avg height). Call [onJumped] on the next frame to ensureVisible/scroll
|
||||||
|
/// to the exact target.
|
||||||
|
void jumpToEstimatedOffset({
|
||||||
|
required int unreadCount,
|
||||||
|
required int totalMessages,
|
||||||
|
required VoidCallback onJumped,
|
||||||
|
}) {
|
||||||
|
if (!hasClients || totalMessages == 0) return;
|
||||||
|
final maxExtent = position.maxScrollExtent;
|
||||||
|
final jumpOffset = maxExtent * (unreadCount / totalMessages);
|
||||||
|
if (jumpOffset > 100) {
|
||||||
|
jumpTo(jumpOffset);
|
||||||
|
}
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => onJumped());
|
||||||
|
}
|
||||||
|
|
||||||
void scrollToBottomIfAtBottom() {
|
void scrollToBottomIfAtBottom() {
|
||||||
// Only scroll if jump button is NOT showing (i.e., already at bottom)
|
// Only scroll if jump button is NOT showing (i.e., already at bottom)
|
||||||
if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) {
|
if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
import '../utils/emoji_utils.dart';
|
||||||
|
|
||||||
|
IconData contactTypeIcon(int type) {
|
||||||
|
switch (type) {
|
||||||
|
case advTypeChat:
|
||||||
|
return Icons.chat;
|
||||||
|
case advTypeRepeater:
|
||||||
|
return Icons.cell_tower;
|
||||||
|
case advTypeRoom:
|
||||||
|
return Icons.group;
|
||||||
|
case advTypeSensor:
|
||||||
|
return Icons.sensors;
|
||||||
|
default:
|
||||||
|
return Icons.device_unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color contactTypeColor(int type) {
|
||||||
|
switch (type) {
|
||||||
|
case advTypeChat:
|
||||||
|
return Colors.blue;
|
||||||
|
case advTypeRepeater:
|
||||||
|
return Colors.orange;
|
||||||
|
case advTypeRoom:
|
||||||
|
return Colors.purple;
|
||||||
|
case advTypeSensor:
|
||||||
|
return Colors.green;
|
||||||
|
default:
|
||||||
|
return Colors.grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color colorForName(String name) {
|
||||||
|
const colors = [
|
||||||
|
Colors.blue,
|
||||||
|
Colors.green,
|
||||||
|
Colors.orange,
|
||||||
|
Colors.purple,
|
||||||
|
Colors.pink,
|
||||||
|
Colors.teal,
|
||||||
|
Colors.indigo,
|
||||||
|
Colors.cyan,
|
||||||
|
Colors.amber,
|
||||||
|
Colors.deepOrange,
|
||||||
|
];
|
||||||
|
return colors[name.hashCode.abs() % colors.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
String firstCharacterOrEmoji(String name) {
|
||||||
|
if (name.isEmpty) return '?';
|
||||||
|
final emoji = firstEmoji(name);
|
||||||
|
if (emoji != null) return emoji;
|
||||||
|
final runes = name.runes.toList();
|
||||||
|
if (runes.isEmpty) return '?';
|
||||||
|
return String.fromCharCode(runes[0]).toUpperCase();
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
class Cyr2Lat {
|
||||||
|
static Map<String, String> _charMap = {
|
||||||
|
'А': 'A',
|
||||||
|
'В': 'B',
|
||||||
|
'Е': 'E',
|
||||||
|
'Ё': 'E',
|
||||||
|
'З': '3',
|
||||||
|
'К': 'K',
|
||||||
|
'М': 'M',
|
||||||
|
'Н': 'H',
|
||||||
|
'О': 'O',
|
||||||
|
'Р': 'P',
|
||||||
|
'С': 'C',
|
||||||
|
'Т': 'T',
|
||||||
|
'Х': 'X',
|
||||||
|
'Ь': 'b',
|
||||||
|
'а': 'a',
|
||||||
|
'е': 'e',
|
||||||
|
'ё': 'e',
|
||||||
|
'о': 'o',
|
||||||
|
'р': 'p',
|
||||||
|
'с': 'c',
|
||||||
|
'у': 'y',
|
||||||
|
'х': 'x',
|
||||||
|
};
|
||||||
|
|
||||||
|
static final RegExp _prefixRegExp = RegExp(r'\@\[[\S\s]+\] ');
|
||||||
|
|
||||||
|
static void setCharMap(Map<String, String> charMap) {
|
||||||
|
_charMap = Map.from(charMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String encode(String text) {
|
||||||
|
if (text.isEmpty) return text;
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
|
||||||
|
final senderName = extractSenderName(text);
|
||||||
|
final msgText = removeSenderName(text);
|
||||||
|
|
||||||
|
for (final rune in msgText.runes) {
|
||||||
|
final char = String.fromCharCode(rune);
|
||||||
|
buffer.write(_charMap[char] ?? char);
|
||||||
|
}
|
||||||
|
|
||||||
|
return senderName + buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String removeSenderName(String text) {
|
||||||
|
final match = _prefixRegExp.matchAsPrefix(text);
|
||||||
|
if (match != null) {
|
||||||
|
return text.substring(match.end);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String extractSenderName(String text) {
|
||||||
|
final match = _prefixRegExp.matchAsPrefix(text);
|
||||||
|
if (match != null) {
|
||||||
|
return match.group(0) ?? '';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
class GifHelper {
|
||||||
|
/// Parse a known GIF format, which can be any of:
|
||||||
|
/// g:GIFID
|
||||||
|
/// https://media.giphy.com/media/GIFID/giphy.gif
|
||||||
|
/// https://giphy.com/gifs/Optional-title-with-dashes-GIFID
|
||||||
|
///
|
||||||
|
/// GIFID is a Giphy GIF ID. The https:// is optional (and
|
||||||
|
/// can also be http://). The giphy.com/gifs form can also
|
||||||
|
/// include a trailing slash.
|
||||||
|
///
|
||||||
|
/// Returns null if text is not a valid GIF format
|
||||||
|
static String? parseGif(String text) {
|
||||||
|
final trimmed = text.trim();
|
||||||
|
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
|
||||||
|
if (match != null) {
|
||||||
|
return match.group(1);
|
||||||
|
}
|
||||||
|
final directUrlMatch = RegExp(
|
||||||
|
r'^(?:https?:\/\/)?media\.giphy\.com\/media\/([A-Za-z0-9_-]+)\/giphy\.gif$',
|
||||||
|
).firstMatch(trimmed);
|
||||||
|
if (directUrlMatch != null) {
|
||||||
|
return directUrlMatch.group(1);
|
||||||
|
}
|
||||||
|
// Giphy understands page URLs with just the ID, or any string and a
|
||||||
|
// dash before the ID, and redirects to a page with a dash-separated
|
||||||
|
// title, a dash, and the ID. IDs in this form *probably* can't
|
||||||
|
// contain dashes.
|
||||||
|
final pageMatch = RegExp(
|
||||||
|
r'^(?:https?:\/\/)?giphy\.com\/gifs\/(?:[^/?]*-)?([A-Za-z0-9_]+)\/?$',
|
||||||
|
).firstMatch(trimmed);
|
||||||
|
return pageMatch?.group(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode a GIF in a format that parseGif() can parse.
|
||||||
|
static String encodeGif(String gifId) {
|
||||||
|
return 'g:$gifId';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
|
import '../l10n/l10n.dart';
|
||||||
|
import '../widgets/image_send_codec_binding.dart';
|
||||||
|
import '../widgets/image_send_preview_sheet.dart';
|
||||||
|
|
||||||
|
/// Shared "attach an image" flow, used by both the direct-message and channel
|
||||||
|
/// chat screens so the two surfaces cannot drift apart.
|
||||||
|
///
|
||||||
|
/// Picks a photo, then shows [ImageSendPreviewSheet] so the user sees the
|
||||||
|
/// packet count and airtime *before* committing to a send that may occupy the
|
||||||
|
/// channel for seconds. Returns null if the user backed out at either step.
|
||||||
|
///
|
||||||
|
/// The caller owns the actual transmission: this only produces the payload.
|
||||||
|
Future<ImageSendPreviewResult?> pickAndPreviewImage({
|
||||||
|
required BuildContext context,
|
||||||
|
required ImageSendCodec codec,
|
||||||
|
ImagePicker? picker,
|
||||||
|
}) async {
|
||||||
|
final XFile? picked;
|
||||||
|
try {
|
||||||
|
picked = await (picker ?? ImagePicker()).pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
// The codec centre-crops to 512x512 anyway, so there is nothing to gain
|
||||||
|
// from decoding a 12 MP original -- but stay well above 512 so the crop
|
||||||
|
// still has detail to work with.
|
||||||
|
maxWidth: 2048,
|
||||||
|
maxHeight: 2048,
|
||||||
|
);
|
||||||
|
} on Exception catch (e) {
|
||||||
|
debugPrint('image pick failed: $e');
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.chat_imagePickFailed)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (picked == null) return null; // user cancelled the picker
|
||||||
|
|
||||||
|
final Uint8List bytes;
|
||||||
|
int originalBytes;
|
||||||
|
try {
|
||||||
|
bytes = await picked.readAsBytes();
|
||||||
|
originalBytes = bytes.length;
|
||||||
|
if (!kIsWeb) {
|
||||||
|
// length() is the on-disk size, which is what we want to show against the
|
||||||
|
// transmitted size; fall back to the in-memory length if it fails.
|
||||||
|
try {
|
||||||
|
originalBytes = await File(picked.path).length();
|
||||||
|
} on FileSystemException {
|
||||||
|
// keep the in-memory length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} on Exception catch (e) {
|
||||||
|
debugPrint('image read failed: $e');
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.l10n.chat_imagePickFailed)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.mounted) return null;
|
||||||
|
|
||||||
|
return showImageSendPreviewSheet(
|
||||||
|
context: context,
|
||||||
|
imageBytes: bytes,
|
||||||
|
originalFileBytes: originalBytes,
|
||||||
|
codec: codec,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,29 +1,35 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../utils/platform_info.dart';
|
import '../utils/platform_info.dart';
|
||||||
|
import '../helpers/snack_bar_builder.dart';
|
||||||
|
|
||||||
class LinkHandler {
|
class LinkHandler {
|
||||||
|
static TextStyle defaultLinkStyle(BuildContext context, TextStyle base) {
|
||||||
|
final brightness = Theme.of(context).brightness;
|
||||||
|
final orange = brightness == Brightness.dark
|
||||||
|
? const Color(0xFFFFB74D)
|
||||||
|
: const Color(0xFFE65100);
|
||||||
|
return base.copyWith(color: orange, decoration: TextDecoration.underline);
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a [SelectableLinkify] on desktop or a [Linkify] on mobile.
|
/// Returns a [SelectableLinkify] on desktop or a [Linkify] on mobile.
|
||||||
static Widget buildLinkifyText({
|
static Widget buildLinkifyText({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required String text,
|
required String text,
|
||||||
required TextStyle style,
|
required TextStyle style,
|
||||||
TextStyle? linkStyle,
|
TextStyle? linkStyle,
|
||||||
|
VoidCallback? onSecondaryTap,
|
||||||
}) {
|
}) {
|
||||||
final effectiveLinkStyle =
|
final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
|
||||||
linkStyle ??
|
|
||||||
style.copyWith(
|
|
||||||
color: Colors.green,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
);
|
|
||||||
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
|
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
|
||||||
const linkifiers = [UrlLinkifier()];
|
const linkifiers = [UrlLinkifier(), EmailLinkifier()];
|
||||||
void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
|
void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
|
||||||
|
|
||||||
if (PlatformInfo.isDesktop) {
|
if (PlatformInfo.isDesktop) {
|
||||||
return SelectableLinkify(
|
final linkify = SelectableLinkify(
|
||||||
text: text,
|
text: text,
|
||||||
style: style,
|
style: style,
|
||||||
linkStyle: effectiveLinkStyle,
|
linkStyle: effectiveLinkStyle,
|
||||||
@@ -31,6 +37,14 @@ class LinkHandler {
|
|||||||
linkifiers: linkifiers,
|
linkifiers: linkifiers,
|
||||||
onOpen: onOpen,
|
onOpen: onOpen,
|
||||||
);
|
);
|
||||||
|
if (onSecondaryTap == null) return linkify;
|
||||||
|
return Listener(
|
||||||
|
onPointerDown: (event) {
|
||||||
|
if (event.buttons & kSecondaryMouseButton != 0) onSecondaryTap();
|
||||||
|
},
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
child: linkify,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Linkify(
|
return Linkify(
|
||||||
text: text,
|
text: text,
|
||||||
@@ -90,21 +104,19 @@ class LinkHandler {
|
|||||||
final uri = Uri.parse(url);
|
final uri = Uri.parse(url);
|
||||||
if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
|
if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showDismissibleSnackBar(
|
||||||
SnackBar(
|
context,
|
||||||
content: Text(context.l10n.chat_couldNotOpenLink(url)),
|
content: Text(context.l10n.chat_couldNotOpenLink(url)),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showDismissibleSnackBar(
|
||||||
SnackBar(
|
context,
|
||||||
content: Text(context.l10n.chat_invalidLink),
|
content: Text(context.l10n.chat_invalidLink),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import '../models/contact.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import '../connector/meshcore_protocol.dart';
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
import '../models/contact.dart';
|
||||||
|
|
||||||
class PathHelper {
|
class PathHelper {
|
||||||
static String formatPathHex(List<int> pathBytes) {
|
static String formatPathHex(List<int> pathBytes) {
|
||||||
@@ -8,24 +10,82 @@ class PathHelper {
|
|||||||
.join(',');
|
.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
static String resolvePathNames(
|
static String hopHex(int byte) {
|
||||||
List<int> pathBytes,
|
return byte.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||||
List<Contact> allContacts,
|
}
|
||||||
) {
|
|
||||||
return pathBytes
|
static String formatHopHex(List<int> hopBytes) {
|
||||||
.map((b) {
|
return hopBytes
|
||||||
final hex = b.toRadixString(16).padLeft(2, '0').toUpperCase();
|
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||||
|
.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? hopName(int byte, List<Contact> allContacts) {
|
||||||
final matches = allContacts
|
final matches = allContacts
|
||||||
.where(
|
.where(
|
||||||
(c) =>
|
(c) =>
|
||||||
c.publicKey.first == b &&
|
c.publicKey.isNotEmpty &&
|
||||||
|
c.publicKey.first == byte &&
|
||||||
(c.type == advTypeRepeater || c.type == advTypeRoom),
|
(c.type == advTypeRepeater || c.type == advTypeRoom),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
if (matches.isEmpty) return hex;
|
if (matches.isEmpty) return null;
|
||||||
if (matches.length == 1) return matches.first.name;
|
if (matches.length == 1) return matches.first.name;
|
||||||
return matches.map((c) => c.name).join(' | ');
|
return matches.map((c) => c.name).join(' | ');
|
||||||
})
|
}
|
||||||
.join(' \u2192 ');
|
|
||||||
|
static List<Uint8List> splitPathBytes(
|
||||||
|
List<int> pathBytes,
|
||||||
|
int hashByteWidth,
|
||||||
|
) {
|
||||||
|
if (pathBytes.isEmpty) return const [];
|
||||||
|
|
||||||
|
final width = hashByteWidth.clamp(1, 4).toInt();
|
||||||
|
final hops = <Uint8List>[];
|
||||||
|
for (int i = 0; i < pathBytes.length; i += width) {
|
||||||
|
final endIdx = (i + width).clamp(0, pathBytes.length).toInt();
|
||||||
|
final hopBytes = pathBytes.sublist(i, endIdx);
|
||||||
|
if (hopBytes.isNotEmpty) {
|
||||||
|
hops.add(Uint8List.fromList(hopBytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hops;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves path bytes to contact names, supporting multi-byte hash widths.
|
||||||
|
///
|
||||||
|
/// Groups path bytes according to [hashByteWidth]:
|
||||||
|
/// - 1: Single byte per hop (256 unique nodes)
|
||||||
|
/// - 2: Two bytes per hop (65K unique nodes)
|
||||||
|
/// - 3: Three bytes per hop (16M unique nodes)
|
||||||
|
/// - 4: Four bytes per hop (4.3G unique nodes)
|
||||||
|
static String resolvePathNames(
|
||||||
|
List<int> pathBytes,
|
||||||
|
List<Contact> allContacts,
|
||||||
|
int hashByteWidth,
|
||||||
|
) {
|
||||||
|
if (pathBytes.isEmpty) return '';
|
||||||
|
|
||||||
|
final parts = <String>[];
|
||||||
|
|
||||||
|
for (final hopBytes in splitPathBytes(pathBytes, hashByteWidth)) {
|
||||||
|
final hex = formatHopHex(hopBytes);
|
||||||
|
|
||||||
|
final matches = allContacts.where((c) {
|
||||||
|
if (c.publicKey.length < hopBytes.length) return false;
|
||||||
|
if (c.type != advTypeRepeater && c.type != advTypeRoom) return false;
|
||||||
|
return listEquals(c.publicKey.sublist(0, hopBytes.length), hopBytes);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
if (matches.isEmpty) {
|
||||||
|
parts.add(hex);
|
||||||
|
} else if (matches.length == 1) {
|
||||||
|
parts.add(matches.first.name);
|
||||||
|
} else {
|
||||||
|
parts.add(matches.map((c) => c.name).join(' | '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' \u2192 ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
import '../connector/meshcore_protocol.dart';
|
||||||
|
import 'path_helper.dart';
|
||||||
|
import '../models/contact.dart';
|
||||||
|
|
||||||
|
class PathHopResolver {
|
||||||
|
const PathHopResolver._();
|
||||||
|
|
||||||
|
static List<Contact?> resolve({
|
||||||
|
required List<int> pathBytes,
|
||||||
|
required List<Contact> contacts,
|
||||||
|
LatLng? endpoint,
|
||||||
|
bool resolveFromEnd = false,
|
||||||
|
int pathHashByteWidth = 1,
|
||||||
|
}) {
|
||||||
|
final width = pathHashByteWidth.clamp(1, 4).toInt();
|
||||||
|
final candidatesByPrefix = <String, List<Contact>>{};
|
||||||
|
for (final contact in contacts) {
|
||||||
|
if (contact.publicKey.length < width) continue;
|
||||||
|
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final prefix = PathHelper.formatHopHex(
|
||||||
|
contact.publicKey.sublist(0, width),
|
||||||
|
);
|
||||||
|
candidatesByPrefix.putIfAbsent(prefix, () => <Contact>[]).add(contact);
|
||||||
|
}
|
||||||
|
for (final candidates in candidatesByPrefix.values) {
|
||||||
|
candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
|
||||||
|
}
|
||||||
|
|
||||||
|
final hops = PathHelper.splitPathBytes(pathBytes, width);
|
||||||
|
final resolved = List<Contact?>.filled(hops.length, null);
|
||||||
|
final indexes = resolveFromEnd
|
||||||
|
? List<int>.generate(hops.length, (i) => hops.length - 1 - i)
|
||||||
|
: List<int>.generate(hops.length, (i) => i);
|
||||||
|
final distance = Distance();
|
||||||
|
var previousPosition = endpoint;
|
||||||
|
|
||||||
|
for (final index in indexes) {
|
||||||
|
final candidates =
|
||||||
|
candidatesByPrefix[PathHelper.formatHopHex(hops[index])];
|
||||||
|
if (candidates == null || candidates.isEmpty) continue;
|
||||||
|
|
||||||
|
var bestIndex = 0;
|
||||||
|
if (previousPosition != null && candidates.length > 1) {
|
||||||
|
double? nearestDistance;
|
||||||
|
for (var i = 0; i < candidates.length; i++) {
|
||||||
|
final position = _positionOf(candidates[i]);
|
||||||
|
if (position == null) continue;
|
||||||
|
final candidateDistance = distance(previousPosition, position);
|
||||||
|
if (nearestDistance == null || candidateDistance < nearestDistance) {
|
||||||
|
nearestDistance = candidateDistance;
|
||||||
|
bestIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final contact = candidates.removeAt(bestIndex);
|
||||||
|
resolved[index] = contact;
|
||||||
|
previousPosition = _positionOf(contact) ?? previousPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
static LatLng? _positionOf(Contact contact) {
|
||||||
|
if (!contact.hasLocation ||
|
||||||
|
contact.latitude == null ||
|
||||||
|
contact.longitude == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return LatLng(contact.latitude!, contact.longitude!);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,4 +109,9 @@ class ReactionHelper {
|
|||||||
|
|
||||||
return ReactionInfo(targetHash: match.group(1)!, emoji: emoji);
|
return ReactionInfo(targetHash: match.group(1)!, emoji: emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encode a reaction message that parseReaction() can parse.
|
||||||
|
static String encodeReaction(String hash, String emojiIndex) {
|
||||||
|
return 'r:$hash:$emojiIndex';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
// showDismissibleSnackBar shows a [SnackBar] with tap to dismiss
|
||||||
|
// all other properties are default and optional
|
||||||
|
void showDismissibleSnackBar(
|
||||||
|
BuildContext context, {
|
||||||
|
Key? key,
|
||||||
|
required Widget content,
|
||||||
|
Color? backgroundColor,
|
||||||
|
double? elevation,
|
||||||
|
EdgeInsetsGeometry? margin,
|
||||||
|
EdgeInsetsGeometry? padding,
|
||||||
|
double? width,
|
||||||
|
ShapeBorder? shape,
|
||||||
|
HitTestBehavior? hitTestBehavior,
|
||||||
|
SnackBarBehavior? behavior,
|
||||||
|
SnackBarAction? action,
|
||||||
|
double? actionOverflowThreshold,
|
||||||
|
bool? showCloseIcon,
|
||||||
|
Color? closeIconColor,
|
||||||
|
Duration? duration,
|
||||||
|
bool? persist,
|
||||||
|
Animation<double>? animation,
|
||||||
|
void Function()? onVisible,
|
||||||
|
DismissDirection? dismissDirection,
|
||||||
|
Clip? clipBehavior,
|
||||||
|
}) {
|
||||||
|
// Callers often reach here after an async gap; the context may already be
|
||||||
|
// unmounted, or deactivated (popped but not yet disposed) — ancestor
|
||||||
|
// lookups on a deactivated element throw. Showing nothing is the right
|
||||||
|
// outcome in both cases.
|
||||||
|
if (!context.mounted) return;
|
||||||
|
var isActive = true;
|
||||||
|
assert(() {
|
||||||
|
isActive = (context as Element).debugIsActive;
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
if (!isActive) return;
|
||||||
|
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||||
|
if (messenger == null) return;
|
||||||
|
messenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
key: key,
|
||||||
|
content: GestureDetector(
|
||||||
|
onTap: () => messenger.hideCurrentSnackBar(),
|
||||||
|
child: content,
|
||||||
|
),
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
elevation: elevation,
|
||||||
|
margin: margin,
|
||||||
|
padding: padding,
|
||||||
|
width: width,
|
||||||
|
shape: shape,
|
||||||
|
hitTestBehavior: hitTestBehavior,
|
||||||
|
behavior: behavior,
|
||||||
|
action: action,
|
||||||
|
actionOverflowThreshold: actionOverflowThreshold,
|
||||||
|
showCloseIcon: showCloseIcon,
|
||||||
|
closeIconColor: closeIconColor,
|
||||||
|
duration: duration ?? const Duration(seconds: 4),
|
||||||
|
persist: persist,
|
||||||
|
animation: animation,
|
||||||
|
onVisible: onVisible,
|
||||||
|
dismissDirection: dismissDirection ?? DismissDirection.down,
|
||||||
|
clipBehavior: clipBehavior ?? Clip.hardEdge,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,8 +4,14 @@ import 'package:flutter/services.dart';
|
|||||||
|
|
||||||
class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
|
class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
|
||||||
final int maxBytes;
|
final int maxBytes;
|
||||||
|
final String Function(String)? encoder;
|
||||||
|
|
||||||
const Utf8LengthLimitingTextInputFormatter(this.maxBytes);
|
const Utf8LengthLimitingTextInputFormatter(this.maxBytes, {this.encoder});
|
||||||
|
|
||||||
|
int _effectiveByteLength(String text) {
|
||||||
|
final effective = encoder != null ? encoder!(text) : text;
|
||||||
|
return utf8.encode(effective).length;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TextEditingValue formatEditUpdate(
|
TextEditingValue formatEditUpdate(
|
||||||
@@ -13,8 +19,7 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
|
|||||||
TextEditingValue newValue,
|
TextEditingValue newValue,
|
||||||
) {
|
) {
|
||||||
if (maxBytes <= 0) return oldValue;
|
if (maxBytes <= 0) return oldValue;
|
||||||
final bytes = utf8.encode(newValue.text);
|
if (_effectiveByteLength(newValue.text) <= maxBytes) return newValue;
|
||||||
if (bytes.length <= maxBytes) return newValue;
|
|
||||||
|
|
||||||
final truncated = _truncateToMaxBytes(newValue.text, maxBytes);
|
final truncated = _truncateToMaxBytes(newValue.text, maxBytes);
|
||||||
return TextEditingValue(
|
return TextEditingValue(
|
||||||
@@ -25,6 +30,14 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _truncateToMaxBytes(String text, int limit) {
|
String _truncateToMaxBytes(String text, int limit) {
|
||||||
|
if (encoder != null) {
|
||||||
|
final runes = text.runes.toList();
|
||||||
|
while (runes.isNotEmpty &&
|
||||||
|
_effectiveByteLength(String.fromCharCodes(runes)) > maxBytes) {
|
||||||
|
runes.removeLast();
|
||||||
|
}
|
||||||
|
return String.fromCharCodes(runes);
|
||||||
|
}
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
var used = 0;
|
var used = 0;
|
||||||
for (final rune in text.runes) {
|
for (final rune in text.runes) {
|
||||||
|
|||||||