Compare commits

..

441 Commits

Author SHA1 Message Date
zjs81 c78c77309e Merge remote-tracking branch 'origin/main' 2026-08-11 00:34:19 -07:00
zjs81 724696cd3c Remove 'jni' from FLUTTER_FFI_PLUGIN_LIST in generated_plugins.cmake files for Linux and Windows
Update version in pubspec.yaml to 9.5.0+16
2026-08-11 00:17:02 -07:00
zjs81 cf6c93c50a rebuild translations 2026-08-11 00:15:45 -07:00
Zach eee72b4c65 Add ability to send images over lora 2026-08-10 23:20:46 -07:00
Zach 736eb064bf Update RadioSettings preset with new spreading factor and coding rate 2026-08-09 19:55:01 -07:00
zjs81 9e6968aa04 Merge pull request #509 from ericszimmermann/ez_maps_high_contrast_PR
add High Contrast filter for maps
2026-08-09 19:35:15 -07:00
zjs81 8904fc7208 Merge pull request #514 from Hacuchino-hash/device-hardware-info
Show connected node hardware and firmware in device info
2026-08-09 19:34:31 -07:00
zjs81 02d3325d0d Merge pull request #517 from Hacuchino-hash/hv-battery-chemistry
Add high voltage LiPo battery chemistry option
2026-08-09 19:31:12 -07:00
Hacuchino-hash 3a08f52fd4 Add high voltage LiPo battery chemistry option 2026-08-07 08:02:01 -05:00
Hacuchino-hash 3a9c14e946 Show connected node hardware and firmware in device info 2026-08-07 08:02:00 -05:00
ericz b15f6450bd dart format 2026-08-04 17:27:51 +02:00
ericz 2879bf6147 fix test/screen/path_trace_test.dart 2026-08-04 17:23:50 +02:00
ericz 36f12ebded add High Contrast filter for maps 2026-07-30 19:26:33 +02:00
zjs81 98f6cf3745 Merge pull request #506 from Warmax356/patch-1
Add SWHID and Archive badges to README
2026-07-28 07:51:25 -07:00
WarMax356 35e4d7fa6c Add SWHID and Archive badges to README
Added SWHID and Archive badges to the README.
2026-07-23 13:04:58 +02:00
zjs81 0fe2502309 Merge pull request #450 from PacoX/dev
Add Support for Multi-Byte Routing Paths
2026-07-09 07:14:53 -07:00
PacoX a8cac76491 resolved conflict and update doc 2026-07-09 09:08:50 +02:00
PacoX 2df17be673 remplace onReorder by onReorderItem 2026-07-09 08:43:55 +02:00
Zach 71fa8c17a4 Add cap for discovered contacts to manage memory usage 2026-07-08 20:48:09 -07:00
Zach 698a69dc03 Improve path formatting in message bubble to use zero-width space for better visual presentation 2026-07-08 20:43:58 -07:00
Zach 8765d0f4cc Add localization for 'Received a GIF' in multiple languages and update chat UI 2026-07-08 20:38:06 -07:00
zjs81 c683dbd8f1 Update funding sources in FUNDING.yml 2026-07-08 20:07:45 -07:00
zjs81 b8e113d269 Merge pull request #467 from sethoscope/set-repeater-key
Add UI for setting repeater identity keys
2026-07-06 09:23:01 -07:00
Seth Golub e963248e37 Stop repeater identity key search when user navigates away
Terminate a search for a repeater key with a specified prefix if the
user navigates away from the settings screen.

This commit also adds dispose() calls for the new repeater key input
controllers, which were previously omitted.
2026-07-04 18:54:27 -07:00
Seth Golub 68d2250afc Fix randomBytes to use specified length
Previously we were creating a 32-byte list no matter what was asked for.
2026-07-04 18:34:11 -07:00
Zach f4cca2fb1d Add ExportOptions.plist for App Store Connect configuration
- Created a new ExportOptions.plist file to define export settings for app distribution.
- Configured the plist with method, team ID, signing style, symbol upload option, and destination.
2026-07-04 00:45:18 -07:00
zjs81 0f3961f3a2 Merge pull request #461 from ericszimmermann/ez_alternative_maps
Alternative Maps
2026-07-04 00:32:58 -07:00
zjs81 08130350d7 Merge pull request #497 from ericszimmermann/ez_zerohop_location_advert_PR
Zero-Hop Location advert
2026-07-04 00:31:52 -07:00
ericszimmermann c1dea86dd1 Merge branch 'zjs81:dev' into ez_zerohop_location_advert_PR 2026-07-01 07:27:44 +02:00
zjs81 e1b8d6ec97 Merge pull request #491 from km-foss/chore/au-mid-preset
Add Australia (Mid) Radio Preset
2026-06-25 18:03:34 -07:00
PacoX 3541b1bbac Fix room login screen 2026-06-24 14:38:35 +02:00
PacoX ef6c1e0898 Fix login screen 2026-06-24 14:24:12 +02:00
km-foss eb89ad1d50 chore: Add Australia (Mid) Radio Preset
This mirrors the config option from the official Meshcore app, and represents the preset now commonly in use in New South Wales.
2026-06-24 09:21:45 +10:00
ericz 5ef976b3b5 Add option for automatic zerohop adverts on posision change. 2026-06-23 18:19:22 +02:00
zjs81 7b4f90d646 Merge pull request #484 from hamid-elaosta/icon-fix
Fix adaptive icon and app label on Android
2026-06-19 22:30:34 -07:00
Seth Golub e1a4900dc0 Add UI for setting repeater identity keys
This adds a simple UI in repeater management settings for remotely
setting a repeater's private key. A key can be pasted from an external
key generator, or a random one can be generated here, optionally with a
specified prefix.

If a prefix is specified, searching is done in 30ms batches in order
to keep the UI responsive and show the progress indicator animation
smoothly. Searches can be interrupted. An estimation of search length is
given as expected number of generated key pairs. (A future improvement
would be to observe keys per second performance and give time estimates
based on that.)

When it generates a new private key on your behalf, it can tell you
what the corresponding public key will be. Unfortunately, if you paste
one in from another source, it can't compute the corresponding public
key. This is because the standard Dart crypto library doesn't provide
that capability, nor does it give access to the elliptic curve functions
it implements so we could do it ourselves. Our only recourse would be to
copy the source for all those things, and I didn't think it was worth
it.

This will not check the validity of a pasted key, but the remote
repeater will. It does limit it to the correct length and hexadecimal
charset.

Prefixes are limited to 4 characters. There's no technical reason for
this, but users are unlikely to want more, and I think it would just be
a bad experience to allow more and spin for a very long time. If someone
really wants a longer one, they can use an external generator like they
do now.

Possible TODO: rewrite background processing using isolates.

Closes #142.
2026-06-19 07:27:44 -07:00
Hamid Elaosta 7fc253cc88 Add adaptive Android icon support and fix app label 2026-06-18 01:18:32 +01:00
PacoX 445a18a83f Fix failed test 2026-06-16 15:01:10 +02:00
PacoX 8273c416b5 Merge pull request #5 from HDDen/update-pacox-multibyte
Updated with dev again
2026-06-16 13:38:05 +02:00
HDDen b44c69f411 Merge branch 'dev' of https://github.com/PacoX/meshcore-open into update-pacox-multibyte 2026-06-16 12:11:34 +03:00
HDDen 6492058a04 Merge branch 'dev' into update-pacox-multibyte 2026-06-16 11:53:59 +03:00
zjs81 5670ab0067 Merge pull request #468 from HDDen/test-regions
Regions: adapted to last dev and add regions fetching from repeaters (continued from PR#379)
2026-06-15 22:53:42 -07:00
zjs81 98a9197cb1 Merge remote-tracking branch 'origin/dev' into test-regions 2026-06-15 22:52:31 -07:00
zjs81 55da952bfd Merge remote-tracking branch 'origin/dev' into test-regions Also added fixes 2026-06-15 22:46:59 -07:00
zjs81 9b4742951d Merge pull request #479 from MrSurly/478-implement-right-click
Issue 478: Implement right click for desktop use
2026-06-15 21:56:13 -07:00
Zach 43f3d439ba Add jni to FFI plugin list for Linux and Windows 2026-06-15 13:12:08 -07:00
PacoX ed294decfb Merge pull request #4 from HDDen/update-pacox-multibyte
Updated to dev
2026-06-15 14:43:27 +02:00
Eric Poulsen 5f54a2cd1b dart format 2026-06-13 22:05:46 -07:00
Eric Poulsen e1536c49b1 tap --> secondaryTap, per review comments 2026-06-13 20:43:30 -07:00
Eric Poulsen bfa62523df Prefer GestureDetector over Listener for right-click in routing sheet 2026-06-13 20:43:13 -07:00
Eric Poulsen 6813a72767 Fix null-returning ternaries with if blocks, per code review comments 2026-06-13 20:32:05 -07:00
Eric Poulsen dbd3a40bdc Rename _handleMapTap to _handleMapContextPress, per review comments. 2026-06-13 20:29:36 -07:00
Eric Poulsen 3009036565 Remove white space per code review 2026-06-13 20:27:58 -07:00
Eric Poulsen 80f4fd5218 Added HitTestBehavior.opaque, per code review comments 2026-06-13 20:26:38 -07:00
Eric Poulsen 321d4b9775 Use kSecondaryMouseButton bitmask check, per code review 2026-06-13 20:23:32 -07:00
Eric Poulsen 1c183d7e67 Add right-click support to routing sheet path history rows 2026-06-13 19:46:00 -07:00
Eric Poulsen a1e6f6967c Added support for left-click copy, and snackbar message about copying the data. 2026-06-13 19:30:27 -07:00
Eric Poulsen f34c2a92c3 Add onSecondaryTap to MeshCard and fix right-click on message text bubbles 2026-06-13 19:22:16 -07:00
Eric Poulsen f1478722b0 Fix for right-click on message bubbles (right click didn't work on the actual text of the bubble) 2026-06-13 19:05:52 -07:00
Eric Poulsen c867225073 onSecondaryTap for line_of_sight_map_screen 2026-06-13 16:37:12 -07:00
Eric Poulsen 06285a02ff onSecondaryTap for map_screen 2026-06-13 16:36:55 -07:00
HDDen e92d215582 upd - fix and format 2026-06-14 01:36:49 +03:00
HDDen d61eaaaff1 Merge branch 'dev' into update-pacox-multibyte 2026-06-14 01:12:49 +03:00
ericz ecde5e1109 fix StadiaMaps Dark and add demo. 2026-06-13 20:01:43 +02:00
ericz f548f95e29 merge zjs81/dev: resolve conflicting implementations 2026-06-13 18:06:31 +02:00
zjs81 9957e1fc1f Merge pull request #474 from zjs81/dev
Merge Dev into main
2026-06-13 02:14:19 -07:00
zjs81 e38d03a32e Fix battery voltage null check in RepeaterStatusScreen 2026-06-13 02:13:34 -07:00
zjs81 ea657a964a Update documentation and dependencies for MeshCore Open
- Replaced sqflite with shared_preferences for local key-value storage in README.md
- Updated gradle.properties to include builtInKotlin and newDsl flags
- Enhanced translation feature documentation in additional-features.md
- Modified BLE protocol documentation to include new command and response codes in ble-protocol.md
- Clarified channel management details in channels.md
- Improved chat and messaging documentation, including message path viewing and translation options in chat-and-messaging.md
- Updated contacts management details in contacts.md
- Revised map and location documentation for inferred locations and user interface changes in map-and-location.md
- Adjusted navigation flow in navigation.md to reflect changes in screen transitions
- Updated notification system details in notifications.md
- Enhanced repeater management documentation in repeater-management.md
- Clarified scanner and connection process in scanner-and-connection.md
- Reorganized settings documentation for better clarity and added new node and location settings in settings.md
2026-06-13 02:12:00 -07:00
zjs81 760d8e1db3 Update localization files and improve path map UI
- Added new localization strings for Swedish, Ukrainian, and Chinese languages in app_sv.arb, app_uk.arb, and app_zh.arb respectively.
- Enhanced the path map UI in channel_message_path_screen.dart for better readability and interaction.
- Improved message retry logic in message_retry_service.dart to prevent double-pumping of queues.
- Bumped version number in pubspec.yaml to 9.5.0+13.
- Cleared untranslated strings in untranslated.json to reflect current localization status.
2026-06-13 01:58:42 -07:00
zjs81 880df84828 Merge pull request #473 from zjs81/dev
Merge Dev into main
2026-06-13 00:42:09 -07:00
zjs81 815534d409 Merge pull request #472 from zjs81/ui
Add shared UI components and refactor MeshCoreConnector
2026-06-13 00:39:46 -07:00
zjs81 becfbedc99 format files 2026-06-13 00:39:13 -07:00
zjs81 7da4e68384 remove md 2026-06-13 00:38:00 -07:00
zjs81 5ea6b17b16 feat: enhance MeshCoreConnector with improved timeout calculation and path resolution; add PathHopResolver for better contact resolution 2026-06-13 00:36:45 -07:00
zjs81 3707acb124 Refactor code structure and remove redundant sections for improved readability and maintainability 2026-06-12 22:55:41 -07:00
zjs81 51d6210920 Add shared UI components for mesh application
- Introduced `mesh_ui.dart` with reusable widgets including SectionHeader, MeshCard, StatusChip, StatTile, AvatarCircle, SignalBars, RouteChip, PulseDot, BottomSheetHeader, ErrorRetryCard, and ListEntrance.
- Implemented `path_map_ui.dart` for path map screens, featuring path distance calculations, playback controls, and a summary list of observed paths.
- Created `themed_map_tile_layer.dart` for shared cached map tiles with automatic dark-mode treatment.
2026-06-12 21:04:02 -07:00
HDDen 27695d372a Updated to last dev 2026-06-12 14:44:09 +03:00
HDDen d06a4c1861 Merge branch 'dev' into update-pacox-multibyte 2026-06-12 13:39:09 +03:00
Zach 6a31d304d3 feat: update license type to nonprofit and add jni to plugin lists; bump dependencies for flutter_local_notifications, package_info_plus, share_plus, flutter_blue_plus_platform_interface, and llamadart 2026-06-11 10:08:31 -07:00
zjs81 26fdf74d69 Refactor UI code for better readability and consistency
- Improved formatting of ListTile icons and text styles in settings_screen.dart, telemetry_screen.dart, usb_screen.dart, gif_picker.dart, path_editor_sheet.dart, repeater_login_dialog.dart, and room_login_dialog.dart for better readability.
- Consolidated TextStyle definitions into single lines where applicable.
- Updated notification_service.dart to enhance readability of notification ID assignment.
- Simplified function signatures in routing_sheet.dart for clarity.
- Cleaned up test assertions in usb_flow_test.dart for conciseness.
- Removed unused translations in untranslated.json to streamline localization files.
2026-06-11 00:28:13 -07:00
zjs81 b1de1b4bf0 chore: untrack .claude/worktrees gitlinks
Remove the accidentally committed Claude Code worktree gitlinks.
.claude/ is already gitignored (prev commit). Worktree files remain
on disk locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:11:56 -07:00
zjs81 3c26ce2d93 chore: stop tracking .claude worktrees and ignore .claude/
The previous commit accidentally added the Claude Code worktree
gitlinks under .claude/worktrees. Untrack them and ignore .claude/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:10:49 -07:00
zjs81 cba1e5950c feat: add contact UI helpers and path editor for routing management
- Implemented contactTypeIcon and contactTypeColor functions for better UI representation of contact types.
- Created colorForName and firstCharacterOrEmoji functions to enhance contact display.
- Developed PathEditorSheet widget for managing contact paths with a user-friendly interface.
- Introduced RoutingSheet for managing contact routing modes and displaying path history.
- Added a script for generating proof of concept (PoC) payloads for clipboard contact import validation.
2026-06-11 00:07:12 -07:00
zjs81 743ef7f124 Merge pull request #470 from jirogit/fix/ja-repeater-translation
fix(l10n): correct ja repeater terminology
2026-06-10 07:17:06 -07:00
me 13d3a107da fix(l10n): replace flood/repeater machine translations in Japanese
- 洪水 → フラッド (flood routing terminology)
- リピーター → リピータ (consistent katakana without long vowel)
- 中継局 → リピータ
- 繰り返し送信装置 → リピータ
- オフグリッド...の繰り返し → オフグリッドリピータ
- 最大浸水範囲の回数 → フラッドパケットの最大ホップ数
- インバウンドフラッパケット → インバウンドフラッドパケット (typo fix)
- ルーティングループに見えるような、洪水パケットを送信する → ルーティングループを検知する
- カスタムパスには、メッセージを中継できる中間地点が必要です → リピータが必要です
- ワンホップでの広告放送(中継なし)→(リピータなし)
2026-06-08 16:18:18 -07:00
me dfdcafb071 fix(l10n): correct ja repeater terminology
Replace machine-translated repeater terms with consistent Japanese:
- 繰り返し送信装置 → リピーター (map, listFilter)
- 繰り返し設定 → リピーターを自動追加 (contactsSettings)
- ホップの繰り返し → リピーターホップ (channelPath)
- 繰り返し送信する、近隣 → 近隣のリピーター (neighbors)
- 近くの電波中継局 → 近くのリピーター (snrIndicator)
- 送信装置名 → リピーター名 (repeater settings)
- オフグリッド...の繰り返し → オフグリッドリピーター (settings)
- 中継装置およびルームサーバーの設置場所 → リピーター/ルームサーバーの位置情報
- 繰り返し送信に関する情報 → リピーターに関する情報 (repeater_guest)
2026-06-08 15:30:43 -07:00
PacoX c46a5ffa2f Fix direct path override validation 2026-06-05 08:34:57 +02:00
PacoX a15f5e94da fix: path formatting in DM mode 2026-06-04 20:26:46 +02:00
HDDen 9d1251f21e Merge branch 'dev' into test-regions 2026-05-31 16:52:27 +03:00
HDDen 0ca96ce5b1 Regions discovery from nearby repeaters 2026-05-31 16:47:22 +03:00
zjs81 33b3b04294 Merge pull request #459 from HDDen/telemetry-gps-map
Telemetry screen: now included map with received gps mark and autoupdate feature
2026-05-30 19:21:47 -07:00
HDDen b72ade1387 Update channel_chat_screen.dart 2026-05-31 00:22:04 +03:00
HDDen 3fc038682c Merge commit 'refs/pull/379/head' of https://github.com/zjs81/meshcore-open into test-regions 2026-05-31 00:06:16 +03:00
PacoX 9e5b360f28 fix int conversion 2026-05-28 09:31:42 +02:00
PacoX 89360c8bc2 fix channels_screen 2026-05-28 09:18:42 +02:00
ericszimmermann 28df5a97e8 Merge branch 'dev' into ez_alternative_maps 2026-05-28 08:56:54 +02:00
PacoX 2e767730fd Merge branch 'dev' into dev 2026-05-28 08:51:23 +02:00
PacoX b7fe5331d0 Merge pull request #3 from HDDen/update-pacox-multibyte
Updated multibyte
2026-05-28 08:43:18 +02:00
HDDen 61fb39b9f8 Multibyte: some path fixes
- Validate encoded path hop counts before saving or sending custom paths
- Fix multi-byte path reversal by reversing hops instead of raw bytes
- Decode raw log path lengths using packed path_len format
- Localize path hash mode options and optimize map anchor lookup
2026-05-28 03:17:08 +03:00
HDDen 9e18b99e9d Multibyte: fixed path trace repeater icon and potential bugs 2026-05-28 02:52:04 +03:00
HDDen 2e4f26d7cd Multibyte: fix map 2026-05-28 02:20:56 +03:00
PacoX 6e81fdfd46 add clamp for path length 2026-05-27 08:32:16 +02:00
PacoX 5d03f99040 fix tracepath 2026-05-26 20:12:27 +02:00
HDDen 6a7dd981a2 Fix «Dispose the telemetry map controller on widget teardown» 2026-05-26 21:01:18 +03:00
HDDen d68f755677 Merge branch 'dev' into telemetry-gps-map 2026-05-26 20:58:57 +03:00
zjs81 264d2bcc9a Merge pull request #462 from HDDen/sync-progressbar
Onstart syncronization progressbar
2026-05-26 10:15:00 -07:00
PacoX e88281b7b6 fix case-sensitive prefix 2026-05-26 09:04:38 +02:00
PacoX 3a11e35a7a fix path trace overlay 2026-05-26 08:50:54 +02:00
HDDen 8dd385beed Merge branch 'dev' into sync-progressbar 2026-05-26 00:44:08 +03:00
HDDen 2328848400 Onstart sync progressbar: fix potential bug with spinner on interrupt synchronization 2026-05-26 00:37:42 +03:00
ericz 29e551d6cb add Documentation 2026-05-25 22:24:38 +02:00
ericz 531b85b8aa add OSM Auto Map 2026-05-25 11:56:01 +02:00
PacoX 3d956596a5 Merge pull request #2 from ericszimmermann/ez_snr_multibyte2.1
Forgot the bytewise reverse of the outgoing path
2026-05-25 08:30:12 +02:00
zjs81 0287de1862 Merge pull request #438 from ericszimmermann/ez_translate_notification
Translate Notifications
2026-05-24 15:47:47 -07:00
HDDen 4dd472e3c3 Onstart sync progressbar: changed default screen to channels
Because it's synchronization is faster and first in line
2026-05-24 15:33:54 +03:00
ericz bdd0d3dee2 make raster tile source scollable 2026-05-24 11:55:50 +02:00
ericz 11a4f9a373 try OpenStreetMap Dark 2026-05-24 11:37:56 +02:00
ericz acbe4dcc49 remove inversal for outgoing path 2026-05-24 10:04:44 +02:00
ericz 9b99eadcf7 forgot the bytewise reverse of the outgoing path 2026-05-23 23:28:01 +02:00
HDDen ed0e6b6554 Onstart sync progressbar: init 2026-05-23 21:06:52 +03:00
HDDen 6d258154a0 fix Flutter SDK update
PR #458 included
2026-05-23 18:28:16 +03:00
ericz 09a83c4f22 add high dpi stadiamaps 2026-05-23 17:21:23 +02:00
PacoX 5486ae8ccb fix multibyte path in private message and repeter 2026-05-23 13:32:11 +02:00
ericz fc67d8d5aa show cached tiles 2026-05-23 13:10:59 +02:00
ericz 225d07b440 add stadiamaps.com 2026-05-23 13:10:23 +02:00
ericz bac82dc9e8 Fix Flutter SDK update 2026-05-21 00:11:54 +02:00
ericz 8682e6ea67 fix missing _handleQueuedMessageReceived after merge of dev 2026-05-21 00:02:21 +02:00
ericszimmermann 30a1a36ee4 Merge branch 'dev' into ez_translate_notification 2026-05-20 23:26:05 +02:00
ericz 3fe5cdf55d update to current dev a50c0d0b2d 2026-05-20 23:20:16 +02:00
ericz 9ada4ea601 add toggle for autmatically translated messages for notification and chat or manual translation on message action. Due to heavy battery usage. 2026-05-20 21:24:54 +02:00
PacoX 945a118853 expose mode 3 in UI 2026-05-20 08:50:17 +02:00
HDDen 7a823654df Telemetry: room-server request fix #2
The telemetry parser has been expanded and should now support significantly more metrics. It has been ported from the Python implementation of meshcore_py.
2026-05-19 23:17:06 +03:00
HDDen 425229fce8 Telemetry request: map and autorefresh 2026-05-19 20:37:34 +03:00
HDDen c4b3971bdd Squashed commit of the following:
commit 83ffe44025
Author: HDDen <62592944+HDDen@users.noreply.github.com>
Date:   Tue May 19 19:16:48 2026 +0300

    fix Flutter SDK update
2026-05-19 19:17:13 +03:00
PacoX 01d4f002f8 add unit tests 2026-05-19 10:50:18 +02:00
PacoX 9331b02f94 Merge pull request #1 from ericszimmermann/ez_snr_multibyte2
Multibyte Support for SNR-Indicator, channel message_bubble and Setting
2026-05-19 10:14:03 +02:00
PacoX 27f7932db9 fix import 2026-05-19 08:52:44 +02:00
PacoX 4a39a7cd46 fix from copilot review + doc fix 2026-05-19 08:49:52 +02:00
zjs81 a50c0d0b2d Merge pull request #453 from HDDen/mcoa-roomsrv-alfa
Room-server: fixed first message letters trim
2026-05-18 15:49:04 -07:00
ericz 3f1bd54889 fix wrong hop count in channel_message_path_screen observed path chooser. 2026-05-18 22:49:56 +02:00
ericz 91759f531a fix double entry due to mixed 1byte and 2byte paths 2026-05-18 21:30:37 +02:00
ericz 87708e2684 fixes direct repeater mixing 1 byte paths with 2 byte Indicator, and wrong description. 2026-05-18 20:42:40 +02:00
ericz 73e5b80232 add HashMode Setting 2026-05-18 19:17:53 +02:00
ericz 28a5913ba3 multibyte for snr_indicator and map_filtering. 2026-05-18 19:00:07 +02:00
PacoX 091c754584 clean 2026-05-18 09:00:30 +02:00
PacoX cf27c59c84 path aware fix 2026-05-18 08:57:13 +02:00
PacoX 119590434a path aware 2026-05-17 20:14:16 +02:00
PacoX dc82414447 rollback-pr review 2026-05-17 19:37:43 +02:00
PacoX 23f29f2cda rollback-pr review 2026-05-17 17:44:30 +02:00
PacoX d66b16a4e8 PR review modif + packet aware 2026-05-17 15:53:00 +02:00
PacoX d6647f4701 PR review modif + packet aware 2026-05-17 15:50:25 +02:00
HDDen 72448f67d0 Room-server: fixed first message letters trim 2026-05-17 13:20:49 +03:00
ericz bc5f299350 try fix codex sync problem 2026-05-17 10:47:50 +02:00
zjs81 6d97ad6855 Merge pull request #449 from Stempit/add-runssian-presets
Add Russian regional presets
2026-05-15 12:28:10 -07:00
PacoX 87b0fd6fc7 removed distance-based rejection filter 2026-05-14 12:55:46 +02:00
ericz 1fbe1823cb only take translated result if status==completed 2026-05-13 18:06:26 +02:00
PacoX 4a066e08dc fix from PR review 2026-05-13 13:44:42 +02:00
ZIER f941f0dbfa avoid double translation and strip replyInfo from translated text. 2026-05-13 13:20:07 +02:00
PacoX 94ed729985 bug fix 2026-05-13 10:43:57 +02:00
PacoX 8296f44a4e update doc 2026-05-13 09:09:11 +02:00
PacoX 4e70fd2a5e clean 2026-05-13 08:51:44 +02:00
PacoX ca02197414 UI fix 2026-05-13 08:49:24 +02:00
Stempit 352a6c427e Reorder alphabetically 2026-05-13 01:49:51 +03:00
Stempit 5f9259e41f Add Russian regional presets 2026-05-13 01:33:05 +03:00
ZIER 75ae903b99 implement flutter_langdetect 2026-05-12 20:58:46 +02:00
zjs81 8892823337 Merge pull request #444 from sethoscope/fix-channel-chat-icon
Use correct channel icons in channel chat screen
2026-05-12 09:55:30 -07:00
zjs81 e738664f89 Merge pull request #448 from zjs81/main
Main
2026-05-12 09:54:39 -07:00
zjs81 e37616fa15 Merge pull request #145 from pioneer/unread-peoplefirst
Unread badges for tabs
2026-05-12 09:53:33 -07:00
PacoX 6e2abfeb2d multi-byte paths-doc 2026-05-12 16:40:34 +02:00
PacoX b62c1c7338 multi-byte paths-debug 2026-05-12 16:33:33 +02:00
PacoX 17379394bd multi-byte paths 2026-05-12 16:08:19 +02:00
Seth Golub 2763d83fe4 Use correct channel icons in channel chat screen
At the top of the channel chat screen is an icon, indicating the
channel type.

Previously, the public icon was used correctly, but the
hashtag icon was used for all other types.

Now, consistent with the channels screen, we use the lock icon for
private channels, and the composite icons for community public &
community hashtag types.

The fix for private channels was trivial, as we can identify hashtag
channels by their name. Finding out whether a channel belongs to a
community is much more involved. All the hard-working code was copied
from channels_screen.dart. (I tried refactoring to reduce duplication,
but my results were complex and not worth it.)

Closes #432
2026-05-11 21:13:50 -07:00
Serge Tarkovski 77018dc358 Recompute channels unread total after cachedChannels is updated 2026-05-12 00:47:26 +03:00
ericz 1f6b2dacf9 changed translation prompt back to specific input. 2026-05-11 07:15:39 +02:00
zjs81 21c58d4e13 Merge pull request #443 from Maxb0tbeep/dev
Add "NRF52" as a device name prefix
2026-05-10 21:05:59 -07:00
Max Cooley 3af97ff6dd Accidentally wrote quotes instead of backticks...oops 2026-05-10 17:19:57 -07:00
Max Cooley 703d5a1ec4 Add "NRF52" as a device name prefix 2026-05-10 17:12:48 -07:00
ericz d2a6fbe182 translate notifications. 2026-05-10 11:09:25 +02:00
zjs81 e801a497f8 Merge pull request #435 from zjs81/dev
merge dev into main
2026-05-09 19:30:34 -07:00
zjs81 e92a66ff28 Update MeshCoreConnector to optimize GPS response handling and increment version to 9.0.0 2026-05-09 19:29:06 -07:00
zjs81 6900e5c3db Run translations 2026-05-09 19:20:32 -07:00
zjs81 966a8d0d2c Fix CMake configuration for flserial to resolve glibc conflict and remove unused translations from Russian language issue: 280 2026-05-09 19:18:09 -07:00
zjs81 3ec3b05fb8 Merge pull request #400 from HDDen/dev
Feature Request: alternative compression by replace some 2-byte symbols by 1-byte latin analogs (named Cyr2Lat)
2026-05-09 17:45:31 -07:00
zjs81 14a93e9bf5 Add website link to README for easier access 2026-05-09 17:21:06 -07:00
zjs81 c229b0369e Enhance documentation and features
- Updated supported languages to include Hungarian, Japanese, and Korean.
- Added new on-device message translation feature with detailed usage instructions.
- Introduced emoji reactions in chat with user interface and functionality details.
- Implemented linkification for automatic detection of URLs and meshcore URIs in messages.
- Added GPX export functionality for contacts with GPS coordinates.
- Enabled pinch-to-zoom for chat text scaling.
- Documented background service for Android to maintain BLE connection in the background.
- Revised BLE protocol documentation to reflect changes in connection state machine and command codes.
- Updated channels documentation to clarify message display and interaction options.
- Enhanced chat and messaging documentation with new translation button and message metadata.
- Clarified contact actions in contacts documentation.
- Adjusted map and location documentation for improved node name visibility and filter options.
- Revised navigation documentation to streamline disconnection process.
- Improved notification documentation to specify batch notification behavior.
- Updated repeater management documentation to reflect new features and settings.
- Enhanced scanner and connection documentation for device filtering and connection timeout.
- Expanded settings documentation to include new translation options.
- Removed jni plugin references from generated plugin files for Linux and Windows.
2026-05-09 17:19:58 -07:00
HDDen 9f332e93be Merge branch 'dev' of https://github.com/zjs81/meshcore-open into dev 2026-05-09 02:18:00 +03:00
zjs81 b472ea8c70 Merge pull request #424 from zjs81/chan-util
basic repeater chan util
2026-05-08 13:40:28 -07:00
zjs81 a67c6d81c3 Merge pull request #425 from zjs81/tcp-host
reword tcp host
2026-05-08 13:39:25 -07:00
zjs81 91ae4dab90 Merge pull request #426 from zjs81/debug-log
rename ble debug log
2026-05-08 13:39:10 -07:00
zjs81 08ac60a408 Merge pull request #428 from sethoscope/remove-channel-subtitles
Remove channel subtitles from UI
2026-05-08 13:38:33 -07:00
zjs81 d4da34fcf7 Merge pull request #433 from zjs81/gps-toggle-in-settings
Gps toggle in settings
2026-05-08 13:38:08 -07:00
zjs81 74840d3baf Optimistically update currentCustomVars in setCustomVar
Reflect the set value immediately so UI bound to currentCustomVars
(e.g. the GPS toggle in settings) updates on tap rather than waiting
for a later device-info refresh.
2026-05-08 13:37:51 -07:00
zjs81 4a72fbd1ad Apply dart format 2026-05-08 13:37:17 -07:00
zjs81 dbe0a5411b Merge remote-tracking branch 'origin/dev' into gps-toggle-in-settings
# Conflicts:
#	lib/l10n/app_bg.arb
#	lib/l10n/app_de.arb
#	lib/l10n/app_es.arb
#	lib/l10n/app_fr.arb
#	lib/l10n/app_hu.arb
#	lib/l10n/app_it.arb
#	lib/l10n/app_ja.arb
#	lib/l10n/app_ko.arb
#	lib/l10n/app_localizations_es.dart
#	lib/l10n/app_localizations_it.dart
#	lib/l10n/app_localizations_nl.dart
#	lib/l10n/app_localizations_pt.dart
#	lib/l10n/app_localizations_sv.dart
#	lib/l10n/app_localizations_uk.dart
#	lib/l10n/app_nl.arb
#	lib/l10n/app_pl.arb
#	lib/l10n/app_pt.arb
#	lib/l10n/app_ru.arb
#	lib/l10n/app_sk.arb
#	lib/l10n/app_sl.arb
#	lib/l10n/app_sv.arb
#	lib/l10n/app_uk.arb
#	lib/l10n/app_zh.arb
2026-05-08 13:36:16 -07:00
zjs81 dc3325ec46 Refactor repeater status screen and settings screen; add GPS toggle
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:27:05 -07:00
zjs81 a92e57bb64 Revert "Refactor repeater status screen and settings screen; add GPS toggle"
This reverts commit e21f3106d0.
2026-05-08 13:24:22 -07:00
zjs81 e21f3106d0 Refactor repeater status screen and settings screen; add GPS toggle
- Updated _RepeaterStatusScreenState to load status after the first frame to avoid mid-build notifyListeners() calls.
- Removed unused _statusRequestedAt variable and adjusted _clockText() to use repeaterClockAtLogin for time display.
- Enhanced _SettingsScreenState with a GPS toggle switch that updates custom variables for GPS settings.
- Cleaned up RepeaterCommandService by removing redundant pending command checks and adjusted command ID generation.
- Removed jni plugin from generated_plugins.cmake for both Linux and Windows platforms.
2026-05-08 13:20:53 -07:00
HDDen 0dcb5f05f0 Merge branch 'dev' of https://github.com/zjs81/meshcore-open into dev 2026-05-08 01:14:21 +03:00
Ded f501d11ec6 Merge pull request #429 from kingult/fix-427-double-position-write
fix: lat/lon double-write in buildUpdateContactPathFrame
2026-05-07 08:25:08 -07:00
kingult dfcf13a97b fix: lat/lon double-write in buildUpdateContactPathFrame
The function emitted two consecutive 8-byte position blocks instead of
one, producing a frame 8 bytes longer than the documented layout. When
a caller passed lastModified, the firmware parsed the duplicated second
lat as the timestamp, giving wildly wrong "last seen" values on
imported contacts.

Delete the unconditional first block; keep the conditional block that
correctly skips the optional tail when neither location nor
lastModified is set, zero-fills position slots when only lastModified
is present, and appends the optional timestamp.

Adds test/connector/build_update_contact_path_frame_test.dart with
five cases covering all four optional-tail combinations plus the
fixed-point lat/lon encoding.

Fixes #427
2026-05-06 16:03:03 -07:00
Seth Golub ccd23c4b81 Remove channel subtitles from UI
Per issue #418, this commmit removes channel subtitles from the channel
list and from the map screen (deep in the marker sharing). This reduces
visual clutter and allows for more compact lists, and the type of
channel is already indicated by the leading icon.

The subtitles simply said "Public channel", "Hashtag channel", or
"Private channel".

We also remove the relevant localization strings.
2026-05-06 14:06:12 -07:00
Enot (ded) Skelly 00636c9084 rename ble debug log
companion protocol so changed to companion and ref:
BLE/TCP/USB
2026-05-05 15:25:32 -07:00
Enot (ded) Skelly accec1681b reword tcp host
IP was being used but not required, could be domain
name as well.
2026-05-05 15:16:31 -07:00
zjs81 67238468ce Merge pull request #423 from zjs81/offgrid-CR
increase CR for off grid
2026-05-05 15:14:00 -07:00
Enot (ded) Skelly bc5b12f1ef formattting
not mine still
2026-05-05 12:46:01 -07:00
Enot (ded) Skelly c09af98bef basic repeater chan util
just repeater stats for now

total channel utilization over uptime i.e. util =
(rxsecs+txsecs) / upsecs
2026-05-05 12:41:06 -07:00
Enot (ded) Skelly ae32e76563 fix someones formatting 2026-05-05 11:07:32 -07:00
Enot (ded) Skelly 5572c9ee75 increase CR for off grid
default should be higher assuming trees etc
2026-05-05 10:59:36 -07:00
HDDen f6cc000788 Merge branch 'dev' of https://github.com/zjs81/meshcore-open into dev 2026-05-01 09:58:18 +03:00
HDDen 75b0d198bc Update translation_service.dart 2026-05-01 01:50:06 +03:00
HDDen 1947cd9f3e sync fix 2026-05-01 01:42:02 +03:00
zjs81 f1d93bd5e8 Merge pull request #399 from zjs81/contacts-sync
fix issues with contact sync
2026-04-30 15:41:25 -07:00
HDDen f63d50f0da sync last dev with cyr2lat 2026-05-01 01:38:31 +03:00
zjs81 eb597b6c68 Merge pull request #416 from zjs81/dev-DesktopMapControls
Add desktop map controls
2026-04-29 12:32:36 -07:00
zjs81 efe21c4e87 Merge pull request #417 from ericszimmermann/ez_latin_heuristics2
latin languages heuristics
2026-04-29 12:31:23 -07:00
ZIER 38fece3313 replace pattern with String. 2026-04-29 11:51:50 +02:00
ZIER 3af3cce606 latin languages heuristics 2026-04-29 11:04:36 +02:00
Ded 026ec6f7de bump app protocol version as we support v4+ features (#398) 2026-04-28 22:35:48 -07:00
Winston Lowe eb50249b93 Add desktop map controls and improve zoom functionality across multiple screens 2026-04-28 19:26:51 -07:00
HDDen ca6058eccd Merge branch 'dev' of https://github.com/zjs81/meshcore-open into dev 2026-04-28 16:26:42 +03:00
zjs81 99c0ab7e22 Merge pull request #404 from pioneer/ukrainian-translations
Ukrainian translation polished + localized hardcoded strings
2026-04-27 13:24:43 -07:00
zjs81 2950a9a687 Merge branch 'dev' into pr-404-merge 2026-04-27 13:23:53 -07:00
zjs81 1b3de54873 Merge pull request #412 from just-stuff-tm/enhancement/los-obstruction-pinning-411
add selectable LOS obstruction pinning for repeater placement Enhancement #411
2026-04-27 13:13:40 -07:00
zjs81 20a9ef3c2b Merge branch 'dev' into enhancement/los-obstruction-pinning-411 2026-04-27 13:13:19 -07:00
zjs81 a741e12ad1 Merge pull request #413 from ericszimmermann/ez_marker_update_squashed
Improve SharedMarker handling
2026-04-27 13:12:09 -07:00
zjs81 e54f30d6fb Merge pull request #414 from Diadlo/fix/jump_to_unread
Improve work with unread messages
2026-04-27 13:11:00 -07:00
zjs81 e1d23ad2c7 style: dart format
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:09:10 -07:00
zjs81 f07993b367 fix(chat): remove unnecessary Navigator.pop calls after setting unread counts 2026-04-27 13:07:21 -07:00
zjs81 0e5f1a45c4 fix(chat): address mark-as-unread double-pop and missed map entry point
- Remove stray Navigator.pop(context) in _markAsUnread for both contact
  and channel chats so the action no longer exits the conversation
- Thread initialUnreadCount through map discovered-contact "Open Chat"
  button so the unread divider/jump still fires from that entry point

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:07:16 -07:00
Dmitry Polshakov f10aeaeba8 chore(l10n): regenerate localizations for mark-as-unread strings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 15:58:03 +03:00
Dmitry Polshakov 00e4f52d75 feat(chat): add "Mark as Unread" action and unread messages divider
- Add "Mark as Unread" option to message context menu in both
  contact and channel chats
- Show "New messages" divider line between read and unread messages
- Add setContactUnreadCount/setChannelUnreadCount methods to connector

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 15:58:03 +03:00
Dmitry Polshakov 3ea2e4763e fix(chat): fix jump-to-oldest-unread scroll not reaching target message
- Pass initialUnreadCount to chat screens before markRead clears it
- Use two-phase scroll: jumpTo estimated offset to build lazy items,
  then ensureVisible for precise positioning
- Await ensureVisible before clearing scroll guard to prevent
  scrollToBottomIfAtBottom from overriding the animation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 14:07:02 +03:00
ericz 94d9afe8b1 consolidate parsing in single parseMarkerText in map_screen.dart 2026-04-26 01:23:33 +02:00
ericz 7db3a12723 squashed commit for: deduplicate markers, allow for updates on position with same label with drawing line, get marker back after deletion in map through tabbing on icon in poi-message. 2026-04-26 00:13:26 +02:00
just-stuff-tm fcf10b4a73 added strings translategemma didnt translate to proper locallization 2026-04-25 09:11:49 -04:00
just-stuff-tm 7f353490cf contextstream/ is added to .gitignore 2026-04-25 09:07:28 -04:00
just_stuff_tm 46683e0ec2 Delete .contextstream/config.json 2026-04-25 09:04:00 -04:00
just-stuff-tm 4e368d562d add selectable LOS obstruction pinning for repeater placement 2026-04-25 08:56:28 -04:00
HDDen 38f6e42796 just fixed conflict of cyr2lat with PR #405 2026-04-25 02:16:05 +03:00
HDDen f56c28a27d synced with last dev, added profiles for cyr2lat replacement dictionaries 2026-04-25 01:39:28 +03:00
Serge Tarkovski 92d3009eb4 Fix swapped url/desc args in GPX export and add ContactLocalization unit tests 2026-04-25 01:32:43 +03:00
HDDen 7a4ac9ae9b Merge branch 'dev' of https://github.com/zjs81/meshcore-open into dev 2026-04-25 01:16:00 +03:00
Serge Tarkovski f8d00caae0 Validate locale override and use preferred locale list for fallback 2026-04-25 01:03:11 +03:00
Serge Tarkovski e03d80b71f Merge remote-tracking branch 'origin/dev' into ukrainian-translations 2026-04-25 00:48:41 +03:00
Serge Tarkovski b7d0db8d1c Refactor: move Contact UI labels to l10n extension; rename raw getter to typeLabelRaw 2026-04-25 00:29:20 +03:00
Serge Tarkovski 6ae3f612ae Localize Score, fix login dialog overflow, use locale-aware date format in channel chat 2026-04-24 13:58:31 +03:00
zjs81 40d3941aab Merge pull request #405 from zjs81/#401-make-multi-ack-a-toggle
#401 make multi ack a toggle
2026-04-23 23:54:52 -07:00
Zach e53c493e78 update TS 2026-04-23 18:01:35 -07:00
Zach 54e0dae172 Add placeholder for multi-ACKs setting in localization 2026-04-23 17:58:40 -07:00
Zach 066aba7c5d #401 Refactor multi-ACK localization strings and settings UI
- Updated localization files for multiple languages to change the representation of multi-ACK settings from a string with a placeholder to a simple string.
- Removed unnecessary placeholder definitions for multi-ACK in localization files.
- Adjusted the settings screen to replace the slider for multi-ACK with a switch, simplifying the user interface.
- Updated the Podfile.lock to remove the wakelock_plus dependency.
2026-04-23 17:58:15 -07:00
Serge Tarkovski 5e446207c6 Ukrainian translation polished; localized remaining hardcoded UI strings 2026-04-23 17:47:37 +03:00
HDDen 609d0c8dbc Added Cyr2Lat compression by replacing 2-byte cyrillic chars by 1-byte latin 2026-04-22 04:20:20 +03:00
Enot (ded) Skelly 820bac0db0 fix issues with contact sync
this adds the actual last modified timestamp when present, before we used
last advert time as last modified in error

also sets _pendingInitialContactsSync to true on first connect over BLE
2026-04-21 16:44:04 -07:00
Serge Tarkovski d3c7d8e43a Red dot unread indicator in bottom tabs, keep numeric unreads only for the lists; fixed unread indicator wasn't on all screens 2026-04-22 01:57:12 +03:00
Serge Tarkovski 0c1e163b88 Reverted Ukrainian translations, will be in a separate PR 2026-04-21 23:50:08 +03:00
Serge Tarkovski d0d6a34fb5 Restore jni to whatever is in main 2026-04-21 23:44:14 +03:00
Serge Tarkovski 16ce1359d7 Remove unused 'Users first' translation key 2026-04-21 23:10:39 +03:00
Serge Tarkovski 9fe4a3710d Add missing users-first translations for hu/ja/ko and regen outputs 2026-04-21 17:01:51 +03:00
Serge Tarkovski 8611adab1f Run dart format and verify analyze 2026-04-21 16:51:10 +03:00
Serge Tarkovski 7d457cb863 Merge main into unread-peoplefirst
Resolved conflicts by accepting refactored state management from main:
- list_filter_widget.dart: Adopt sealed class pattern for filter actions
- contacts_screen.dart: Move state to UiViewStateService instead of local setState
- device_screen.dart: Accept deletion (consolidated into other screens in main)

Main branch includes significant improvements:
- TCP and USB transport support
- Service-based state management with UiViewStateService
- Translation support with message translation buttons
- Signal UI consistency improvements
- Additional language support (hu, ja, ko)
- Comprehensive test coverage
- Discovery screen refactoring
2026-04-21 16:45:43 +03:00
Serge Tarkovski 297516fc80 Update cached unread total when removing contact unread entries
When contacts are removed in removeContact, _handleContact, or _handleContactAdvert,
subtract their unread count from _cachedContactsUnreadTotal immediately so badge
counts reflect the true total without waiting for a full reload.
2026-04-21 16:34:22 +03:00
zjs81 6b6a881c7a Merge pull request #388 from zjs81/msg-chars
add byte counted text input
2026-04-20 09:17:00 -07:00
ericz 8ef8a38495 change to prepare Outbound Text Functions. 2026-04-17 18:32:14 -07:00
Enot (ded) Skelly ddcda4ba5a keep multiline editing 2026-04-17 14:07:00 -07:00
Zach 5cfe45b953 IOS build changes 2026-04-15 09:06:17 -07:00
ericz b572314ae9 respect smaz encoding in message byte length calculation. 2026-04-15 09:04:08 -07:00
Enot (ded) Skelly e97fb9bd24 add byte counted text input
adds a new widget that counts bytes during entry

configurable limit and shows user both count and limit

provides color feedback

use new widget in chat and channel text entry
2026-04-15 09:04:08 -07:00
zjs81 1c9c089a53 Remove 'jni' from Flutter plugin and FFI plugin lists in generated CMake files 2026-04-14 21:58:39 -07:00
zjs81 cb3b5a84eb Merge pull request #387 from zjs81/dev
translations
2026-04-14 21:39:50 -07:00
zjs81 a4bbeffddc Merge pull request #386 from zjs81/dev_translations
Dev translations
2026-04-14 21:38:52 -07:00
zjs81 37ec8f2f05 Add localization for chat and repeater features in multiple languages
- Added translations for "Send message", "Guest information", and "Guest tools" in Bulgarian, German, Spanish, French, Hungarian, Italian, Japanese, Korean, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese.
- Updated the "Clock synchronization after login" feature subtitle in all affected languages.
- Removed untranslated keys from the untranslated.json file as they have now been localized.
2026-04-14 21:38:12 -07:00
zjs81 39cd6d5514 Merge pull request #385 from zjs81/dev
merge dev to main
2026-04-14 21:04:04 -07:00
zjs81 44eb4fad58 Merge pull request #361 from zjs81/unused-plugin
remove unused macos path_provider_foundation
2026-04-14 21:02:30 -07:00
zjs81 1a209cbcfc Merge pull request #372 from zjs81/group-elem
fix: settings dialog lists
2026-04-14 21:02:08 -07:00
zjs81 33a8f34463 Merge pull request #365 from zjs81/rpt-guest
enh: make repeater admin guest aware
2026-04-14 20:44:14 -07:00
zjs81 ce8e8f0d5b Merge pull request #384 from zjs81/clear_toast
clear toast on tap
2026-04-14 20:42:23 -07:00
Enot (ded) Skelly aa2d0f1927 clear toast on tap
this adds a generator showDismissibleSnackBar which by default allows
tapping to clear snack bar toasts. all SnackBar properties are still
available and the

all callers should now use showDismissibleSnackBar() instead of calling
ScaffoldMessenger.of(context).showSnackBar(SnackBar())
2026-04-14 12:01:42 -07:00
Stephan Rodemeier 0e074fd806 Add region management
This adds region management: the user can manage a list of available regions
and for each channel pick a region from that list to apply to messages.

Region discovery from nearby repeaters will be done in a separate PR.

This is a part of the work needed for #120.
2026-04-14 13:07:50 +02:00
Ded 0757c8e53a Merge pull request #369 from just-stuff-tm/auto-time-sync-349
add auto clock synchronization setting after repeater login
2026-04-13 08:20:01 -07:00
Enot (ded) Skelly add4731d05 fix: settings dialog lists
switched to using RadioListTile instead of ListTile to be more accessible
2026-04-10 15:11:44 -07:00
Enot (ded) Skelly 7dc162d968 temp
translations fix
2026-04-10 14:15:14 -07:00
just-stuff-tm 8ba4bbfbc5 add auto clock synchronization setting after repeater login
Introduced a new setting for automatic clock synchronization after a successful repeater login.
Added localization support for the new feature in multiple languages (Bulgarian, German, English, Spanish, French, Hungarian, Italian, Japanese, Korean, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, Chinese).
Implemented storage service methods to manage the new setting.
Updated the repeater settings screen to include a toggle for the new feature.
Enhanced the repeater login dialog to trigger clock synchronization automatically if the setting is enabled.
2026-04-10 14:25:53 -04:00
Ded cac6abfef1 Fix dev
rebase dev over main and resolve merge conflicts
2026-04-09 10:12:47 -07:00
Enot (ded) Skelly 5354acb1d3 clean up after merge conflicts 2026-04-09 09:57:46 -07:00
Enot (ded) Skelly fae416fb34 Merge branch 'dev' of github.com:zjs81/meshcore-open into dev 2026-04-09 09:50:36 -07:00
Enot (ded) Skelly 69433b6d89 small clean up from PR #275
just removes extraneous assignment to _lastNonRepeatSnapshot and moves
the Navigator pop to after all uses of the context in _RadioSettingsDialog
2026-04-09 09:41:02 -07:00
just-stuff-tm ea3b9609fc fix(settings): use integer Hz comparison, unify snapshot conversion, gate debug logging
- Replace floating-point epsilon frequency comparison with integer Hz
- Add frequencyHz getter and fromMeshCoreSnapshot/toMeshCoreSnapshot
  conversion methods on _RadioSettingsSnapshot
- Move _toUiCodingRate/_toDeviceCodingRate to documented top-level functions
- Gate _logRadioSettingsState behind kDebugMode
- Use integer Hz in == and hashCode for _RadioSettingsSnapshot

Addresses code review findings on preset/off-grid repeat toggle PR.
2026-04-09 09:41:02 -07:00
just-stuff-tm 20a9939314 fix(settings): scope repeat preset memory to saved state 2026-04-09 09:41:02 -07:00
just-stuff-tm c7b7deb0f6 fix(settings): preserve preset across off-grid repeat 2026-04-09 09:41:02 -07:00
just-stuff-tm 82e04e8090 Reapply "Fixed Preset on offgrid repeat toggle enhancemet #183"
This reverts commit 758619bbaa6ce5895c7146bbfc3b89054e759527.
2026-04-09 09:41:02 -07:00
Enot (ded) Skelly f299608296 use l10n strings for discovered menu item 2026-04-09 09:41:02 -07:00
ericz 7dcec5b4ee moved _getRepeaterPrefixMatchNearLocation since I don't need the function anywhere else anymore. 2026-04-09 09:41:02 -07:00
ericszimmermann e4684b585a codex suggested fix: explicit check if contact location is not null
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-09 09:41:02 -07:00
ericz 8386f262e1 reimplement location aware snr-indikator after alpha7 2026-04-09 09:41:02 -07:00
Enot (ded) Skelly 45cd8a56a3 add jni to generated plugins
linux and windows were missing jni which was being added on fresh builds from dev
2026-04-09 09:41:02 -07:00
Enot (ded) Skelly 754f8a6c62 add fvm directory and rc file to gitignore 2026-04-09 09:41:02 -07:00
Enot (ded) Skelly c4f54efd77 add tooltip to send message buttons 2026-04-09 09:41:02 -07:00
Winston Lowe 637e08d22c Update ML timeout handling and adjust distance threshold for path hops 2026-04-09 09:40:24 -07:00
Winston Lowe 32dc0fca22 Refactor contact handling and other improvments (#317)
* Refactor contact filtering and improve localization strings; enhance path trace handling

* Add localization for new CLI commands and update existing strings

* Enhance contact handling and UI updates across multiple screens
add unfiltered contact access and improve last seen resolution

* Add polling interval configuration and improve contact handling

* Reorder command constants for better organization and clarity

* Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens

* Moved RadioStatsIconButton in chat screen for improved UI consistency

* Added indicators to AppBar for channels

* Ignore contacts with self public key in contact handling

* Simplify path removal logic and clean up unused imports in path management dialog

* Enhance path hop resolution by adding distance checks to improve candidate selection accuracy

* Remove unnecessary reset of radio stats poll reference count in polling interval setter
2026-04-09 09:40:06 -07:00
n-kam b5aa294fc1 make unread badge max out at 9999+ not 99+ 2026-04-09 09:30:25 -07:00
Winston Lowe 26516baf67 Update ML timeout handling and adjust distance threshold for path hops 2026-04-09 09:30:25 -07:00
Winston Lowe 4879b136f8 Refactor contact handling and other improvments (#317)
* Refactor contact filtering and improve localization strings; enhance path trace handling

* Add localization for new CLI commands and update existing strings

* Enhance contact handling and UI updates across multiple screens
add unfiltered contact access and improve last seen resolution

* Add polling interval configuration and improve contact handling

* Reorder command constants for better organization and clarity

* Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens

* Moved RadioStatsIconButton in chat screen for improved UI consistency

* Added indicators to AppBar for channels

* Ignore contacts with self public key in contact handling

* Simplify path removal logic and clean up unused imports in path management dialog

* Enhance path hop resolution by adding distance checks to improve candidate selection accuracy

* Remove unnecessary reset of radio stats poll reference count in polling interval setter
2026-04-09 09:30:25 -07:00
Enot (ded) Skelly bdd7fc0cdd remove unused macos path_provider_foundation
added in #299 but appears not needed, flutter removes when building
2026-04-08 14:56:34 -07:00
Ded 5ea044af10 Merge pull request #358 from zjs81/followup-#275
small clean up from PR #275
2026-04-08 10:31:17 -07:00
Enot (ded) Skelly 9d20be1c06 small clean up from PR #275
just removes extraneous assignment to _lastNonRepeatSnapshot and moves
the Navigator pop to after all uses of the context in _RadioSettingsDialog
2026-04-08 10:23:57 -07:00
Ded 9436c2d45a Merge pull request #275 from just-stuff-tm/enhancement/preset-offgrid-repeat-toggle
Enhancement/preset offgrid repeat toggle
2026-04-08 10:10:08 -07:00
Ded 17e55e96bb Merge pull request #357 from zjs81/discovered-text
use l10n strings for discovered menu item
2026-04-08 10:03:10 -07:00
Enot (ded) Skelly e4cfbb57b4 use l10n strings for discovered menu item 2026-04-08 10:01:45 -07:00
Ded d9f9ff58b4 Merge pull request #299 from ericszimmermann/ez_location-snr
reimplement location aware snr-indikator after alpha7
2026-04-08 09:07:02 -07:00
Ded a059f1be45 Merge pull request #356 from zjs81/gen-plug-jni
add jni to generated plugins
2026-04-08 08:38:35 -07:00
Enot (ded) Skelly 9e46f8b44c add jni to generated plugins
linux and windows were missing jni which was being added on fresh builds from dev
2026-04-08 08:37:50 -07:00
Ded a934781009 Merge pull request #355 from zjs81/ignore-fvm
add fvm directory and rc file to gitignore
2026-04-08 08:36:31 -07:00
Enot (ded) Skelly 5fe6738f25 add fvm directory and rc file to gitignore 2026-04-08 08:35:20 -07:00
Ded c1bcf261d7 Merge pull request #353 from zjs81/more-tooltips
add tooltip to send message buttons
2026-04-08 08:32:17 -07:00
Enot (ded) Skelly b570539a2d add tooltip to send message buttons 2026-04-08 08:22:13 -07:00
Ded 89a14c2719 Merge pull request #347 from zjs81/add-contribution
init contributing.md
2026-04-07 14:37:35 -07:00
Enot (ded) Skelly 4ad01ed43c init contributing.md 2026-04-07 13:01:46 -07:00
zjs81 ffaa4033ae Merge pull request #321 from just-stuff-tm/main
Add additional device name prefixes to MeshCoreUuids
2026-04-06 23:04:29 -07:00
zjs81 1a4fd1b477 Merge pull request #339 from ericszimmermann/ez_fix_coordinates
Preserve Coordinates with contact.copyWith() function
2026-04-06 22:58:21 -07:00
zjs81 e1555ce380 Merge pull request #337 from interfect/lowmesh
Add LowMesh prefix and explain how to add more
2026-04-06 22:51:44 -07:00
zjs81 c7933d363b Merge pull request #342 from interfect/graceful-gif-render
Support receiving more formats of GIF message
2026-04-06 14:28:19 -07:00
Zach 08ffb978cf fix: gif trnslat 2026-04-06 14:26:42 -07:00
Adam Novak c5ec60638c Put reaction and GIF helpers in charge of encoding 2026-04-06 02:09:40 -04:00
Adam Novak 75ec3b6116 Centralize GIF parsing in a helper like for reactions 2026-04-06 01:57:51 -04:00
Adam Novak 45c9823c6f Escape forward slashes in regexes 2026-04-05 22:51:48 -04:00
Adam Novak 45658a7612 Understand more kinds of Giphy reference as GIF
This adds Giphy page URLs and `media.giphy.com` URLs (with and without
protocols) as *accepted* encodings for GIF messages, alongside the `g:`
syntax.

When someone posts such a URL by itself as a message, it will be rendered inline just like `g:` messages are now.

This does not change the encoding that GIF messages are *sent* in; that
is still the `g:` syntax.
2026-04-05 22:39:20 -04:00
Winston Lowe a14833494e Merge branch 'dev' of github.com:zjs81/meshcore-open into dev 2026-04-05 12:27:38 -07:00
n-kam 457b44de3a make unread badge max out at 9999+ not 99+ 2026-04-05 12:17:16 -07:00
Winston Lowe 36d4a10396 Update ML timeout handling and adjust distance threshold for path hops 2026-04-05 12:17:15 -07:00
Winston Lowe 77566b0fe1 Refactor contact handling and other improvments (#317)
* Refactor contact filtering and improve localization strings; enhance path trace handling

* Add localization for new CLI commands and update existing strings

* Enhance contact handling and UI updates across multiple screens
add unfiltered contact access and improve last seen resolution

* Add polling interval configuration and improve contact handling

* Reorder command constants for better organization and clarity

* Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens

* Moved RadioStatsIconButton in chat screen for improved UI consistency

* Added indicators to AppBar for channels

* Ignore contacts with self public key in contact handling

* Simplify path removal logic and clean up unused imports in path management dialog

* Enhance path hop resolution by adding distance checks to improve candidate selection accuracy

* Remove unnecessary reset of radio stats poll reference count in polling interval setter
2026-04-05 12:17:15 -07:00
ericz 7633327f45 Previously, the merge only preserved path override fields and could overwrite existing GPS with null when the incoming frame had 0,0 coordinates.
Now it also preserves prior coordinates when the incoming update omits location.
2026-04-05 14:06:23 +02:00
Adam Novak 6b4b2d7ce6 Add LowMesh prefix and explain how to add more 2026-04-04 19:40:39 -04:00
zjs81 10b63e0df2 Merge pull request #334 from zjs81/Local-LLM-Translator
Local llm translator
2026-04-02 22:57:17 -07:00
zjs81 ba6d751346 #256 finalize translation service 2026-04-02 22:52:52 -07:00
zjs81 96d222a580 fix: update translation model ID retrieval and improve file name extraction in translation service 2026-04-02 22:38:31 -07:00
zjs81 01ad8471cc fix: improve message sending logic and handle range download errors in translation service 2026-04-02 19:52:43 -07:00
zjs81 2b826757cb feat: add translation strings for message translation feature 2026-04-02 19:18:19 -07:00
zjs81 9bf649e2c6 feat: add message translation support
- Introduced translation functionality in chat screen, allowing users to translate messages before sending.
- Added MessageTranslationButton to the input bar for enabling/disabling translation.
- Implemented translation service to handle incoming and outgoing text translations using llama models.
- Enhanced message storage to include original and translated text, language codes, and translation status.
- Created UI components for displaying translated messages and managing translation options.
- Added translation model management, including downloading and storing models locally.
- Updated app settings to manage translation preferences and model selections.
2026-04-02 19:09:17 -07:00
zjs81 c7a2bf9a95 Merge pull request #316 from n-kam/unread-badge-max-value
Make unread badge max out at 9999+ instead of 99+
2026-04-01 22:47:31 -07:00
zjs81 82adbd761b Merge pull request #313 from thesebas/pl-lang
new labels fixed polish translations
2026-04-01 22:46:39 -07:00
zjs81 9a8bdf00dc Merge pull request #326 from spfmoby/better-french-translations
Better french translations
2026-04-01 22:45:12 -07:00
zjs81 8b30342113 Merge pull request #329 from dennis1248/main
Update Dutch translations
2026-04-01 16:51:11 -07:00
Winston Lowe 817c60a155 Update ML timeout handling and adjust distance threshold for path hops 2026-03-31 19:02:29 -07:00
Dennis ten Hoove f08e86cf97 Update Dutch translations 2026-03-31 20:09:26 +02:00
spfmoby a6bb9490a1 Better french translations 2026-03-30 09:17:28 +02:00
just-stuff-tm e4e8bfa4ef Add additional device name prefixes to MeshCoreUuids 2026-03-28 12:20:27 -04:00
ericz d1e45fc2ba moved _getRepeaterPrefixMatchNearLocation since I don't need the function anywhere else anymore. 2026-03-28 17:08:59 +01:00
ericz 32fa96431e Merge branch 'main' into ez_location-snr 2026-03-28 17:01:38 +01:00
just-stuff-tm 1e9508d401 fix(settings): use integer Hz comparison, unify snapshot conversion, gate debug logging
- Replace floating-point epsilon frequency comparison with integer Hz
- Add frequencyHz getter and fromMeshCoreSnapshot/toMeshCoreSnapshot
  conversion methods on _RadioSettingsSnapshot
- Move _toUiCodingRate/_toDeviceCodingRate to documented top-level functions
- Gate _logRadioSettingsState behind kDebugMode
- Use integer Hz in == and hashCode for _RadioSettingsSnapshot

Addresses code review findings on preset/off-grid repeat toggle PR.
2026-03-27 11:49:59 -04:00
just-stuff-tm 36697c6e61 fix(settings): scope repeat preset memory to saved state 2026-03-27 11:49:59 -04:00
just-stuff-tm c9145c99d3 fix(settings): preserve preset across off-grid repeat 2026-03-27 11:49:59 -04:00
just-stuff-tm 6b6d9caeeb Reapply "Fixed Preset on offgrid repeat toggle enhancemet #183"
This reverts commit 758619bbaa6ce5895c7146bbfc3b89054e759527.
2026-03-27 11:49:59 -04:00
Winston Lowe d0e3767db6 Refactor contact handling and other improvments (#317)
* Refactor contact filtering and improve localization strings; enhance path trace handling

* Add localization for new CLI commands and update existing strings

* Enhance contact handling and UI updates across multiple screens
add unfiltered contact access and improve last seen resolution

* Add polling interval configuration and improve contact handling

* Reorder command constants for better organization and clarity

* Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens

* Moved RadioStatsIconButton in chat screen for improved UI consistency

* Added indicators to AppBar for channels

* Ignore contacts with self public key in contact handling

* Simplify path removal logic and clean up unused imports in path management dialog

* Enhance path hop resolution by adding distance checks to improve candidate selection accuracy

* Remove unnecessary reset of radio stats poll reference count in polling interval setter
2026-03-26 22:28:01 -07:00
n-kam f9cb0c80a5 make unread badge max out at 9999+ not 99+ 2026-03-27 01:39:52 +03:00
thesebas a26d14bd46 new labels fixed polish translations 2026-03-25 08:36:09 +01:00
zjs81 411cd3f8d2 Merge pull request #270 from just-stuff-tm/fix/linux-ble-pairing-flow
Fix/linux ble pairing flow
2026-03-24 17:48:07 -07:00
just_stuff_tm 38f4de80b6 Refactor Bluetooth pairing localization strings across multiple languages
- Reintroduced Bluetooth pairing PIN title, prompt, show, and hide strings in English, Spanish, French, Hungarian, Italian, Japanese, Korean, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese.
- Updated localization files to ensure consistency and clarity in user prompts related to Bluetooth pairing.
2026-03-24 22:21:23 +00:00
just_stuff_tm 7de07c023f Merge branch 'main' into fix/linux-ble-pairing-flow 2026-03-24 02:24:28 -04:00
zjs81 c272c60f9a Formatted file 2026-03-23 22:37:05 -07:00
zjs81 eca78453d6 Remove debug print statements from MeshCoreConnector, MessageRetryService, and UsbSerialService and fix wrong retry being credited 2026-03-23 22:26:51 -07:00
zjs81 3754cf14ea Bump version to 7.0.0+9 in pubspec.yaml 2026-03-23 19:50:52 -07:00
zjs81 834850fb51 Add companion radio stats, adaptive backoff, path hash width, and UI improvements
- Companion radio stats: poll and display noise floor, RSSI, SNR, airtime
  with dedicated ValueNotifier and ref-counted polling
- Adaptive RF-aware TX backoff based on radio conditions instead of fixed 5s
- Variable-width path hash support (1-3 bytes per hop)
- Air activity dot indicator in app bar with tap to open stats screen
- Jump to oldest unread setting for chat screens
- 1s send cooldown on DM and channel messages
- Link style: theme-aware orange, added EmailLinkifier
- New languages: Hungarian, Japanese, Korean
- Remove dead DeviceScreen and BatteryIndicatorChip
- Remove wakelock_plus dependency
- TX power fields now read as signed int8
2026-03-23 19:26:05 -07:00
zjs81 e7e2bb91b8 Add radio statistics and localization updates
- Implemented radio statistics features in multiple screens including chat, channels, and settings.
- Added localization for new strings in Swedish, Ukrainian, and Chinese.
- Introduced a setting to jump to the oldest unread message in chat and channels.
- Enhanced path management and display for contacts and messages.
- Updated app settings to include new boolean for jumping to the oldest unread message.
- Improved battery indicator and radio stats display in the app bar.
- Removed unused wakelock_plus dependency and updated plugin registrations.
2026-03-23 19:24:27 -07:00
zjs81 4c492f69ef Merge pull request #218 from zjs81/dev-mapOverlap
Show overlaps in public keys of repeaters
2026-03-23 18:51:14 -07:00
zjs81 50f2a8b439 Merge pull request #311 from zjs81/dev
Merge pull request #310 from zjs81/main
2026-03-23 18:50:02 -07:00
zjs81 2c8a15538e Merge branch 'main' into dev-mapOverlap 2026-03-23 18:49:19 -07:00
zjs81 68eeefa04e Merge pull request #307 from ericszimmermann/ez_location_channel_message_path
location aware channel_message_path
2026-03-23 18:47:06 -07:00
zjs81 ebbc367fec Merge pull request #310 from zjs81/main
merge dev
2026-03-23 18:46:40 -07:00
zjs81 2da8995d0b Merge branch 'dev' into main 2026-03-23 18:46:24 -07:00
zjs81 1c376b0056 Merge pull request #309 from zjs81/dev-unifiedData
Unified packet parsing to use BufferReader
2026-03-23 18:41:38 -07:00
zjs81 da70d5fc08 Merge pull request #29 from thesebas/patch-1
Update Polish localization strings for clarity
2026-03-23 18:40:34 -07:00
thesebas f63bc4b787 some minor adjsts 2026-03-23 23:11:51 +01:00
thesebas 9b1f1e1994 make the 'lastSeen' labels shorter to not break the contacts list layout 2026-03-23 23:07:00 +01:00
thesebas 5f475fce4d use correct translation for Advert in another few places 2026-03-23 22:53:09 +01:00
Winston Lowe 0228c38621 fix: Update battery voltage reading and adjust path length handling in ChannelMessage 2026-03-23 11:24:33 -07:00
Winston Lowe fc7283f076 Update lib/l10n/app_bg.arb
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-23 11:18:37 -07:00
thesebas 7eff1df6e2 use correct word for repeater 2026-03-23 18:47:18 +01:00
Winston Lowe 58252b8a40 fix: Correct return type of _manualAckHash and improve hash computation 2026-03-23 10:14:30 -07:00
Winston Lowe 630606acdc Update byte skipping logic and improve clarity in MeshCoreConnector and ChannelMessage 2026-03-23 08:14:46 -07:00
thesebas bd030153c1 update new labels 2026-03-22 21:58:36 +01:00
thesebas 5140ff383d fix plural form of the label 2026-03-22 21:52:04 +01:00
thesebas dc57f9b9c0 fix missing labels 2026-03-22 21:52:04 +01:00
thesebas 53cd3f4461 Some additional label adjustments 2026-03-22 21:52:04 +01:00
thesebas 35e296f1cd Fix rebase merge error 2026-03-22 21:51:43 +01:00
thesebas 532401cc94 Refactor code structure for improved readability and maintainability 2026-03-22 21:51:19 +01:00
thesebas 5321974cbb Update Polish localization strings for consistency and clarity 2026-03-22 21:51:19 +01:00
thesebas 7c16dde989 Update Polish localization strings 2026-03-22 21:51:19 +01:00
Sebastian Szymbor 9a75c912af Update Polish localization strings for clarity 2026-03-22 21:51:19 +01:00
Winston Lowe 767dc1164e refactor: Replace string reading methods with CString equivalents and improve error handling 2026-03-22 10:50:11 -07:00
just-stuff-tm 14f3429eb5 fix: correct casing of "WisCore-" in deviceNamePrefixes list 2026-03-21 21:07:56 -04:00
just-stuff-tm e49e80d330 style: format deviceNamePrefixes list for better readability 2026-03-21 20:59:54 -04:00
just-stuff-tm d07372c7e0 feat: add MeshCoreUuids class for UUID constants and device name prefixes 2026-03-21 20:59:54 -04:00
just-stuff-tm 990f2bd33d addressed copilot issues still need pr #301 for smoke tests to pass 2026-03-21 20:59:54 -04:00
just-stuff-tm 29660d520e feat: Linux BLE pairing support via bluetoothctl
Add Linux BLE pairing helper that drives bluetoothctl for pair/trust/PIN
entry, with Completer-based flow control, explicit retry loop, and named
timeout constants.

- LinuxBlePairingService: pair-and-trust with up to 2 retries
- LinuxBleErrorClassifier: map bluetoothctl stderr to user-facing errors
- Conditional import stub for web builds (dart.library.io gate)
- Scanner screen: PIN dialog integration for Linux pairing flow
- MeshCoreConnector: Linux pairing/recovery/reconnect wiring
- l10n: 4 new pairing keys across all 14 locales
- 12 unit tests (pairing service + error classifier)
2026-03-21 20:59:53 -04:00
Winston Lowe dbefb0b5f4 feat: Enhance MeshCoreConnector with storage metrics and improve error handling
- Added storageUsedKb and storageTotalKb properties to MeshCoreConnector.
- Updated battery and storage frame parsing with improved error handling.
- Refactored log RX data handling to use BufferReader for better readability and error management.
- Enhanced message parsing in ChannelMessage and Message classes to utilize BufferReader.
- Introduced new text type for signed messages in meshcore_protocol.dart.
- Updated BLE debug log screen to use BufferReader for payload parsing.
- Refactored message retry service to handle ACK hashes as integers instead of Uint8List.
- Improved message storage serialization and deserialization to accommodate new expectedAckHash type.
- Added wasPulled property to Contact model for better state management.
2026-03-21 13:01:02 -07:00
Winston Lowe 4f609f160f feat: Add location validation and improve contact latitude/longitude handling 2026-03-21 09:39:03 -07:00
Winston Lowe e313bea3fc Remove unused _sendAdvert method from SettingsScreen 2026-03-21 09:38:52 -07:00
Winston Lowe 77be2b8e6f Refactor code structure for improved readability and maintainability 2026-03-20 18:58:58 -07:00
Winston Lowe c81c3efe7c Add show overlaps in public keys of repeaters functionality and localization support 2026-03-20 18:57:46 -07:00
Winston Lowe cac0cc15eb feat: Enhance privacy settings and telemetry (#308)
* feat: Enhance privacy settings and telemetry

- Implemented telemetry options for contacts, allowing users to enable or disable telemetry data sharing.
- Introduced a clear chat option in the chat interface for better message management.
- Updated the telemetry screen to handle telemetry data for contacts, including battery level.
- Refactored contact settings to include telemetry options and improved UI for better user experience.

* feat: Refactor repeater resolution logic across multiple screens
2026-03-20 18:54:26 -07:00
Winston Lowe 1392c2d00f feat: Enhance privacy settings and telemetry (#308)
* feat: Enhance privacy settings and telemetry

- Implemented telemetry options for contacts, allowing users to enable or disable telemetry data sharing.
- Introduced a clear chat option in the chat interface for better message management.
- Updated the telemetry screen to handle telemetry data for contacts, including battery level.
- Refactored contact settings to include telemetry options and improved UI for better user experience.

* feat: Refactor repeater resolution logic across multiple screens
2026-03-20 18:34:42 -07:00
zjs81 cb63b48b78 Add comprehensive documentation for various app features
- Introduced "Contacts" documentation detailing the contact management system, types, list, search, and tap actions.
- Added "Map & Location" documentation covering map features, interactions, path tracing, and line-of-sight analysis.
- Created "Navigation" documentation outlining app flow, QuickSwitchBar, and device screen interactions.
- Developed "Notifications" documentation explaining notification types, in-app badges, settings, and rate limiting.
- Established "Repeater Management" documentation for managing repeaters and room servers, including CLI access and telemetry.
- Compiled "Scanner & Connection" documentation detailing BLE, USB, and TCP connection processes.
- Formulated "Settings" documentation outlining access, layout, device info, app settings, node settings, actions, debug options, export features, and about section.
2026-03-20 02:24:02 -07:00
zjs81 4ad4a93a20 formatted code 2026-03-20 01:55:08 -07:00
zjs81 4962a48e64 Msg Retry fixes, channel message fixes. Notification fixes. Make more desktop friendly. Enhance retry algo. Fix predicted location clustering add retries to reactions and fix the reactions in private DMS centralize and cleanup code in var areas 2026-03-20 01:54:31 -07:00
ericszimmermann b88e5e647a Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 08:06:22 +01:00
ericszimmermann 87d11c2e6b Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 07:00:16 +01:00
ericz 7b3c099736 reduce zoomlevel 2026-03-18 06:52:08 +01:00
ericz 11cb14a925 focus on hop if you click on one in the legend. 2026-03-17 23:22:23 +01:00
ericszimmermann d2df2b0bed Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 22:23:23 +01:00
ericz 723bf7293c location aware channel_message_path 2026-03-17 21:56:42 +01:00
zjs81 53caec3e14 Merge pull request #301 from just-stuff-tm/fix/tcp-flow-test-missing-provider
fix: provide AppSettingsService in tcp_flow_test
2026-03-16 16:10:29 -07:00
just_stuff_tm 3c440ca3d4 Merge branch 'zjs81:main' into fix/tcp-flow-test-missing-provider 2026-03-15 21:09:02 -04:00
zjs81 8797d8ffde Merge pull request #302 from stphnrdmr/doc/platform-support
Add more explicit platform support table
2026-03-15 15:21:22 -07:00
Stephan Rodemeier faba120823 Add more explicit platform support table
The platform support was a bit vague, this adds a table to better convey
the differences.
2026-03-15 23:01:38 +01:00
just-stuff-tm be690c8194 fix: provide AppSettingsService in tcp_flow_test
TcpScreen.initState reads AppSettingsService from context
to pre-fill host/port fields, but the test helper only
provided MeshCoreConnector. Switch to MultiProvider so
AppSettingsService is also in the widget tree.
2026-03-15 16:48:40 -04:00
ericszimmermann 0ef2194fb0 codex suggested fix: explicit check if contact location is not null
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-15 12:10:47 +01:00
ericz 3664ae34cd reimplement location aware snr-indikator after alpha7 2026-03-15 11:42:46 +01:00
zjs81 64d75dde45 chore: update version to 7.0.0+8 in pubspec.yaml 2026-03-14 18:46:29 -07:00
zjs81 9199aab7f7 Merge pull request #297 from zjs81/dev-improments
Improvements to path tracing and location handling
2026-03-14 18:42:58 -07:00
zjs81 64698e0be6 Merge pull request #295 from ericszimmermann/ez_group_dropdown3
squashed PR for Dropdown Group Menu
2026-03-14 18:05:22 -07:00
zjs81 3dd9037be3 Merge remote-tracking branch 'origin/main' into ez_group_dropdown3
# Conflicts:
#	lib/main.dart
2026-03-14 18:02:31 -07:00
zjs81 566e3aadf8 fix: migrate filter menus to type-safe generics and harden popup dismissal
- Move ContactSortOption/ContactTypeFilter enums to dedicated
  contact_filter_types.dart (re-exported from contact_search.dart)
- Migrate ContactsFilterMenu and DiscoveryContactsFilterMenu to use
  sealed class action types with SortFilterMenu<T> generics, replacing
  int action constants and switch statements
- Guard _closeDropdownAndRun with ModalRoute.isCurrent check to prevent
  accidental dismissal of parent routes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 17:59:48 -07:00
zjs81 054a84031e Merge pull request #296 from zjs81/feature/ml-timeout-prediction
feat: add ML-based adaptive timeout prediction using LinearRegressor
2026-03-14 17:39:22 -07:00
zjs81 fffcff3b74 fix: cancel persist timer on dispose to prevent post-dispose writes 2026-03-14 17:39:01 -07:00
zjs81 b336aedbc5 fix: address PR #296 code review feedback
- Clamp ML predictions between physics floor (raw airtime) and ceiling
  (worst-case formula) so model can never produce unsafe timeouts
- Replace hourOfDay feature with secondsSinceLastRx for network activity
- Remove unused _ContactStats.stdDev and dead model persistence code
- Debounce observation writes (2s) instead of writing on every delivery
- Skip recording observations when pathLength is null to avoid corrupting
  training data
- Add comment explaining global (not per-contact) RX time tracking
- Remove notifyListeners from retrain to avoid unnecessary widget rebuilds
- Run dart format
2026-03-14 17:32:08 -07:00
zjs81 2ee2358ecc feat: add ML-based adaptive timeout prediction using LinearRegressor
Train a linear regression model on actual message delivery times to
predict tighter timeouts, replacing worst-case physics estimates.
Features: path length, message bytes, seconds since last RX, flood mode.
Global model with per-contact blending after 10+ observations per contact.
Falls back to existing physics formula when model has insufficient data.
2026-03-14 16:56:11 -07:00
ericz 86e9b7fe01 squashed commit of ez_group_dropdown 2026-03-15 00:34:09 +01:00
Serge Tarkovski 1b94442ab6 Fix action constant collision: change _actionTogglePrioritizeUsers from 10 to 11 2026-02-27 21:19:13 +02:00
Serge Tarkovski 3ae14781f0 AI translations for "Users first" 2026-02-27 12:58:32 +02:00
Serge Tarkovski ecc496f2af Merge branch 'main' into unread-peoplefirst 2026-02-27 12:57:59 +02:00
Serge Tarkovski 87b25655d0 Package updates from main 2026-02-27 12:43:21 +02:00
Serge Tarkovski c47a4cb622 fix: filter by _shouldTrackUnreadForContactKey when recalculating cached contacts unread total 2026-02-27 12:28:57 +02:00
Serge Tarkovski a30fc439f3 refactor: use UnreadBadge widget in QuickSwitchBar for consistent badge styling 2026-02-27 12:22:26 +02:00
Serge Tarkovski afcc4db405 fix: clamp cached unread totals to prevent negative badge counts
Clamp both _cachedContactsUnreadTotal and _cachedChannelsUnreadTotal
to >= 0 after decrementing in markContactRead() and markChannelRead().
This prevents the totals from going negative if the cache drifts
out-of-sync, which could cause UI badges to display incorrect values.
2026-02-18 20:37:34 +02:00
Serge Tarkovski 87bcb6a6a3 Proper formatting 2026-02-09 17:40:56 +02:00
Serge Tarkovski 68bb031bb6 "Users first" instead of "People first" everywhere 2026-02-09 17:34:18 +02:00
Serge Tarkovski c4f5c7b171 Cache for unread total 2026-02-09 17:18:21 +02:00
Serge Tarkovski 2bce14224d Update generated plugin registrants after merge 2026-02-09 16:27:53 +02:00
Serge Tarkovski fd305fd55b Update generated plugin registrants after merge 2026-02-09 13:19:31 +02:00
Serge Tarkovski d0dd805244 Merge branch 'main' into unread-peoplefirst 2026-02-09 13:16:05 +02:00
Serge Tarkovski 8668564464 Correct unread badges for tabs; people first contacts sort option 2026-02-09 12:56:38 +02:00
328 changed files with 135954 additions and 17796 deletions
+3
View File
@@ -0,0 +1,3 @@
github: [zjs81]
custom:
- "https://paypal.me/zjs82"
+9 -1
View File
@@ -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
@@ -58,6 +61,7 @@ secrets.dart
.DS_Store .DS_Store
.AppleDouble .AppleDouble
.LSOverride .LSOverride
macos/Flutter/GeneratedPluginRegistrant.swift
# iOS # iOS
**/ios/Pods/ **/ios/Pods/
@@ -83,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/
+1 -1
View File
@@ -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.
+247 -46
View File
@@ -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`) |
+71
View File
@@ -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.
+33 -10
View File
@@ -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,12 +48,12 @@ 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
- **BLE Connection**: Scan and connect to MeshCore devices via Bluetooth - **BLE, USB, TCP Connection**: Scan and connect to MeshCore devices via Bluetooth, USB or TCP
- **Device Settings**: Configure radio parameters, power settings, and network options - **Device Settings**: Configure radio parameters, power settings, and network options
- **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves - **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves
- **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon) - **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon)
@@ -75,10 +77,16 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
### Platform Support ### Platform Support
-**Android**: Full support (API 21+) | Feature | Android (API 21+) | iOS (12+) | Linux | Windows | macOS | Web |
-**iOS**: Full support (iOS 12+) |--------------------|:-----------------:|:---------:|:-----:|:-------:|:-----:|:---------------------------------:|
- 🚧 **Desktop**: Limited support (macOS/Linux/Windows) | BLE companion | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
- 🚧 **Web**: Under construction (Chrome) | USB companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ✅ |
| TCP companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ❌<br>(requires websocket bridge) |
| Core Functionality | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Mesh Network | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Map & Location | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Device Management | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Repeater Hub | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
### Dependencies ### Dependencies
@@ -86,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
@@ -144,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
@@ -178,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
@@ -189,6 +207,7 @@ Messages are transmitted as binary frames using a custom protocol optimized for
### App Settings ### App Settings
- **Theme**: System default, light, or dark mode - **Theme**: System default, light, or dark mode
- **Language**: Use one of 15 languages (English, Chinese, French, Spanish, Portuguese, German, Dutch, Polish, Swedish, Italian, Slovak, Slovene, Bulgarian, Russian, Ukrainian)
- **Notifications**: Configurable for messages, channels, and node advertisements - **Notifications**: Configurable for messages, channels, and node advertisements
- **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types - **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types
- **Message Retry**: Automatic retry with configurable path clearing - **Message Retry**: Automatic retry with configurable path clearing
@@ -205,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
[![SWH](https://archive.softwareheritage.org/badge/origin/https://github.com/zjs81/meshcore-open/)](https://archive.softwareheritage.org/browse/origin/?origin_url=https://github.com/zjs81/meshcore-open)
[![SWH](https://archive.softwareheritage.org/badge/swh:1:dir:d37a80b06359730864150ad2aeadd46cce9abd55/)](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
+24 -4
View File
@@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) {
android { android {
namespace = "com.meshcore.meshcore_open" namespace = "com.meshcore.meshcore_open"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = "29.0.14206865"
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17
@@ -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",
)
} }
} }
+19
View File
@@ -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.**
+1 -1
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

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>
+4
View File
@@ -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
+93
View File
@@ -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.
Binary file not shown.
Binary file not shown.
+93
View File
@@ -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.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

+6 -1
View File
@@ -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
+31
View File
@@ -0,0 +1,31 @@
# MeshCore Open - Feature Documentation
MeshCore Open is an open-source Flutter client for MeshCore LoRa mesh networking devices. This documentation covers every user-facing feature, how to access it, and what it does.
## Table of Contents
1. [Scanner & Connection](scanner-and-connection.md) - BLE scanning, USB serial, and TCP connection
2. [Navigation](navigation.md) - App flow, device screen, and quick-switch navigation
3. [Contacts](contacts.md) - Contact management, groups, discovery, and sharing
4. [Chat & Messaging](chat-and-messaging.md) - Direct messages, message status, reactions, and retries
5. [Channels](channels.md) - Broadcast channels, communities, and channel chat
6. [Map & Location](map-and-location.md) - Node map, path tracing, line-of-sight, and offline caching
7. [Settings](settings.md) - Device settings, app settings, radio configuration, and exports
8. [Notifications](notifications.md) - System notifications, unread badges, and notification preferences
9. [Repeater Management](repeater-management.md) - Repeater hub, status, CLI, telemetry, and neighbors
10. [Additional Features](additional-features.md) - GIF picker, localization, debug logs, SMAZ compression, and more
11. [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
MeshCore Open connects to MeshCore LoRa mesh radios over BLE, USB, or TCP. Once connected, users can:
- **Chat** with other mesh nodes via encrypted direct messages
- **Broadcast** on shared channels (public, hashtag, private, or community-scoped)
- **View nodes on a map** with GPS locations, predicted positions, and path traces
- **Manage repeaters** with CLI access, telemetry, neighbor info, and settings
- **Share contacts** via `meshcore://` URIs and QR codes
- **Configure radio settings** including frequency, power, bandwidth, and spreading factor
- **Cache offline maps** for use without internet connectivity
- **Analyze line-of-sight** between nodes with terrain elevation profiles
+262
View File
@@ -0,0 +1,262 @@
# Additional Features
## GIF Picker (Giphy Integration)
### How to Access
In any chat screen (direct or channel), tap the GIF button in the message input bar.
### What the User Sees
A bottom sheet with a search field and a grid of GIF thumbnails.
### Key Interactions
- On open, loads trending GIFs (G-rated, 25 results)
- Type to search and press the keyboard submit button (search triggers on submit, not on each keystroke). Clearing the search field reloads trending GIFs
- On network/API errors, a "Retry" button is shown in-place
- Tap a GIF to select it — the chat input shows an inline preview with an X button to dismiss
- Send the message to transmit the GIF reference (`g:<giphy-id>`)
- Recipients see the GIF rendered inline via Giphy CDN
- "Powered by Giphy" attribution is always shown at the bottom of the picker
- The bottom sheet occupies 70% of screen height
---
## Localization / Multi-Language Support
### How to Access
App Settings → Appearance → Language
### Supported Languages (18)
English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian, Hungarian, Japanese, Korean
### How It Works
- All UI strings go through Flutter's ARB localization system
- Language can follow the system locale or be explicitly overridden
- Changes take effect immediately
---
## Discovered Contacts Screen
### How to Access
From Contacts screen → overflow menu → "Discovered Contacts"
### What the User Sees
A list of nodes heard passively over the air but not yet added as contacts. Each shows:
- Color-coded avatar (by type)
- Name
- Short public key
- Last-seen time
### Key Interactions
- Search bar with debounced filtering
- Sort by last seen or name; filter by type
- **Tap**: Import the contact (adds to your contact list)
- **Long-press**: Add Contact, Copy `meshcore://` URI to clipboard, or Delete
- Overflow menu → "Delete All" (with confirmation)
- Already-known contacts and your own node are filtered out
---
## SMAZ Compression
### What It Is
An optional per-contact and per-channel text compression feature using the SMAZ algorithm (optimized for short English text).
### How to Enable
- **Per contact**: Chat screen → info button → toggle "SMAZ compression"
- **Per channel**: Long-press channel → Edit → toggle "SMAZ compression"
### How It Works
- When enabled, compression is applied using a "compress only if smaller" strategy — the message is only transmitted compressed if the encoded result is actually shorter than the original. Otherwise, the original text is sent uncompressed
- Compressed messages are transmitted with a `s:` prefix followed by base64-encoded data
- Recipients using MeshCore Open will decompress automatically. **Recipients using other software** that is not SMAZ-aware will see garbled `s:...` text
- The codec operates on ASCII. Non-ASCII / non-English text generally does not benefit from compression and may even expand. Best suited for short English messages
- Disabled by default
---
## Community QR Scanner
### How to Access
From Channels screen → "+" FAB → "Scan Community QR"
### What the User Sees
A live QR scanner view with instruction text overlay.
### Key Interactions
- Scan a community QR code shared by another member
- On valid scan: confirmation dialog showing community name and ID
- Option to "Add public channel to device" on join
- If already a member: shows an "Already a member" dialog
- Invalid QR: shows an orange error snackbar
---
## Channel Message Path Viewing
### How to Access
In a channel chat, tap a message bubble (mobile) or use the "Path" action (desktop).
### What the User Sees
- Summary card: sender, time, repeat count, path type, observed hops
- "Other Observed Paths" section (if multiple paths detected)
- "Repeater Hops" section listing each hop with hex prefix, resolved name, and GPS coordinates
### Actions
- **Radar icon**: Opens path trace map for live trace
- **Map icon**: Opens a map with hop markers and polyline
- **Path dropdown**: Switch between observed path variants (if multiple)
---
## Debug Logging
### BLE Debug Log
**Access**: Settings → BLE Debug Log
Two views:
- **Frames**: Each BLE frame with direction, description, hex preview, timestamp. Long-press to copy hex.
- **Raw Log RX**: Decoded LoRa packets with route type, payload type, path bytes, and summary.
### App Debug Log
**Access**: Settings → App Debug Log (must be enabled first in App Settings → Debug)
Structured log entries with level (Info/Warning/Error), tag, message, and timestamp.
Both logs support copy-all and clear operations.
---
## Chrome Required Screen
### When It Appears
Automatically shown on web platforms when a non-Chromium browser is detected.
### What the User Sees
A full-screen informational page explaining that Web Bluetooth requires a Chromium-based browser. No interactive elements — purely informational.
---
## Path History Service
### What It Does (Background Service)
Maintains an in-memory LRU cache of up to 50 contacts, each with up to 100 route history entries, tracking:
- Hop count and trip time
- Success/failure counts and route weights
- Flood vs. direct discovery
### Path Scoring
Paths are scored using a weighted formula: reliability (45%), route weight (20%), latency (25%), and freshness (10%). These weights are internal and not user-configurable. Paths whose weight drops to zero or below are automatically deleted. Flood deliveries that receive an ACK give a weight boost (+0.5) to the specific return path.
Used internally for:
- **Auto route rotation**: Cycles through known paths using configurable weights on retries, with a diversity window to avoid re-using recently tried paths
- **Path selection**: Picks the best-scored path for each retry attempt
- **Flood statistics**: Tracks flood vs. direct discovery ratios
---
## Message Retry Service
### What It Does (Background Service)
Handles reliable delivery of outgoing direct messages:
1. Assigns a UUID and sends immediately. Only one message per contact can be in-flight at a time (avoids overflowing the firmware's 8-entry ACK table); subsequent messages are queued
2. Listens for ACK frames matched via SHA-256 hash of `[timestamp][attempt][text][sender_pubkey]`
3. On timeout, retries with exponential backoff: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s...)
4. Each retry may use a different path (via path history diversity window)
5. After max retries: marks failed but keeps a **30-second grace window** during which a late ACK can still resolve the message to "delivered". Optionally clears the contact's path
6. Reports RTT and path data for quality learning
7. Maintains an ACK hash history (last 50 entries) to handle duplicate ACKs
### Configurable Settings (App Settings → Messaging)
- Max retries (210, default 5)
- Clear path on max retry (on/off)
- Auto route rotation with weight parameters
---
## Timeout Prediction (ML)
### What It Does (Background Service)
An ML-based service that predicts expected delivery timeouts:
- Collects delivery observations (path length, message size, time since last RX, delivery time) in a sliding window of up to 100 observations (oldest evicted first)
- Requires **10 minimum observations** before first training. After that, retrains every 5 new observations
- Applies a **1.5x safety margin** to raw predictions (the actual timeout issued is 1.5× the model's predicted delivery time)
- Features with zero variance are automatically excluded from training
- Blends per-contact statistics with ML predictions
- Falls back to `3000 + 3000 × pathLength` ms when insufficient data. 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)
---
## 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
+262
View File
@@ -0,0 +1,262 @@
# BLE Protocol & Data Layer
This is a technical reference for the communication protocol and data architecture.
## Transport Layer
The app supports three transports, all sharing the same command/response protocol:
| Transport | Method | Implementation |
|---|---|---|
| Bluetooth LE | Nordic UART Service (NUS) GATT | `flutter_blue_plus` |
| USB Serial | Packet-framed serial | `MeshCoreUsbManager` |
| TCP | Packet-framed socket | `MeshCoreTcpConnector` |
### BLE (Nordic UART Service)
- **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`
- **RX Characteristic** (write to device): `6e400002-b5a3-f393-e0a9-e50e24dcca9e`
- **TX Characteristic** (notify from device): `6e400003-b5a3-f393-e0a9-e50e24dcca9e`
Raw `Uint8List` payloads are written directly to the RX characteristic. Writes use "write without response" if supported, falling back to "write with response".
### USB and TCP Framing
Both use a lightweight packet framing codec:
```
TX (host → device): [0x3C][len_lo][len_hi][payload...]
RX (device → host): [0x3E][len_lo][len_hi][payload...]
```
- Frame start: `0x3C` (`<`) for outgoing, `0x3E` (`>`) for incoming
- Length: 2-byte little-endian, payload only
- Max payload: 172 bytes
- TCP: `tcpNoDelay: true` (Nagle disabled), writes serialized to prevent interleaving
## Connection State Machine
```
enum MeshCoreConnectionState {
disconnected,
scanning,
connecting,
connected,
disconnecting,
}
```
## BLE Connection Lifecycle
1. **Scan** with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`):
- `MeshCore-`
- `Whisper-`
- `WisCore-`
- `Seeed`
- `Lilygo`
- `HT-`
- `LowMesh_MC_`
- `NRF52`
2. **Connect** with 15-second timeout (6 seconds on Linux)
3. **Request MTU** 185 bytes (non-web only)
4. **Discover services** and locate NUS
5. **Enable TX notifications** (up to 3 attempts on native)
6. **Subscribe** to TX characteristic for incoming frames
7. **Initial sync**: device info query, time sync, channel sync
## Auto-Reconnect (BLE Only)
On unexpected disconnection, auto-reconnect with exponential backoff:
- Delays: 1s, 2s, 4s, 8s, 16s, 30s, 30s...
- Resets on successful connection
- Disabled for manual disconnects
- Not available for USB or TCP
## Protocol Constants
| Constant | Value | Description |
|---|---|---|
| Max frame size | 172 bytes | BLE/USB/TCP payload limit |
| Public key size | 32 bytes | Ed25519 public key |
| Max path size | 64 bytes | Maximum path data |
| Max name size | 32 bytes | Maximum node name |
| Max text payload | 160 bytes | Firmware `MAX_TEXT_LEN` |
| App protocol version | 4 | Sent in device query |
| Contact frame size | 148 bytes | Fixed-size contact record |
## Command Codes (App → Device)
| Code | Name | Description |
|------|------|-------------|
| 1 | CMD_APP_START | Announce app connection |
| 2 | CMD_SEND_TXT_MSG | Send direct text message |
| 3 | CMD_SEND_CHANNEL_TXT_MSG | Send channel text message |
| 4 | CMD_GET_CONTACTS | Request contact list |
| 5 | CMD_GET_DEVICE_TIME | Query device clock |
| 6 | CMD_SET_DEVICE_TIME | Set device clock |
| 7 | CMD_SEND_SELF_ADVERT | Broadcast own advertisement |
| 8 | CMD_SET_ADVERT_NAME | Set node name |
| 9 | CMD_ADD_UPDATE_CONTACT | Add or update a contact |
| 10 | CMD_SYNC_NEXT_MESSAGE | Request next queued message |
| 11 | CMD_SET_RADIO_PARAMS | Set radio parameters |
| 12 | CMD_SET_RADIO_TX_POWER | Set TX power |
| 13 | CMD_RESET_PATH | Reset contact path |
| 14 | CMD_SET_ADVERT_LATLON | Set advertised location |
| 15 | CMD_REMOVE_CONTACT | Remove a contact |
| 16 | CMD_SHARE_CONTACT | Share contact to mesh |
| 17 | CMD_EXPORT_CONTACT | Export contact as bytes |
| 18 | CMD_IMPORT_CONTACT | Import contact from bytes |
| 19 | CMD_REBOOT | Reboot device |
| 20 | CMD_GET_BATT_AND_STORAGE | Query battery and storage |
| 22 | CMD_DEVICE_QUERY | Query device info |
| 26 | CMD_SEND_LOGIN | Login to repeater/room |
| 27 | CMD_SEND_STATUS_REQ | Request repeater status |
| 30 | CMD_GET_CONTACT_BY_KEY | Get contact by public key |
| 31 | CMD_GET_CHANNEL | Get channel definition |
| 32 | CMD_SET_CHANNEL | Set channel name and PSK |
| 36 | CMD_SEND_TRACE_PATH | Request path trace |
| 38 | CMD_SET_OTHER_PARAMS | Set misc parameters |
| 39 | CMD_SEND_TELEMETRY_REQ | Request sensor telemetry |
| 40 | CMD_GET_CUSTOM_VAR | Get custom variables |
| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable |
| 50 | CMD_SEND_BINARY_REQ | Send binary request |
| 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 |
| 58 | CMD_SET_AUTO_ADD_CONFIG | Set 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)
| Code | Name | Description |
|------|------|-------------|
| 0 | RESP_CODE_OK | Generic success |
| 1 | RESP_CODE_ERR | Generic error |
| 2 | RESP_CODE_CONTACTS_START | Contact list begins |
| 3 | RESP_CODE_CONTACT | Single contact data |
| 4 | RESP_CODE_END_OF_CONTACTS | Contact list complete |
| 5 | RESP_CODE_SELF_INFO | Device self-info response |
| 6 | RESP_CODE_SENT | Message transmitted; carries `[1]=is_flood, [25]=ack_hash, [69]=estimated_timeout_ms` |
| 7 | RESP_CODE_CONTACT_MSG_RECV | Incoming direct message (v2) |
| 8 | RESP_CODE_CHANNEL_MSG_RECV | Incoming channel message (v2) |
| 10 | RESP_CODE_NO_MORE_MESSAGES | No more queued messages |
| 11 | RESP_CODE_EXPORT_CONTACT | Exported contact data |
| 9 | RESP_CODE_CURR_TIME | Current device time |
| 12 | RESP_CODE_BATT_AND_STORAGE | Battery mV (uint16 LE) + storage used/total (uint32 LE each) |
| 13 | RESP_CODE_DEVICE_INFO | Firmware info |
| 16 | RESP_CODE_CONTACT_MSG_RECV_V3 | Incoming direct message (v3) |
| 17 | RESP_CODE_CHANNEL_MSG_RECV_V3 | Incoming channel message (v3) |
| 18 | RESP_CODE_CHANNEL_INFO | Channel definition |
| 21 | RESP_CODE_CUSTOM_VARS | Custom variables |
| 24 | RESP_CODE_STATS | Companion radio stats |
| 25 | RESP_CODE_AUTO_ADD_CONFIG | Auto-add flags |
| 0x80 | PUSH_CODE_ADVERT | Known contact re-seen |
| 0x81 | PUSH_CODE_PATH_UPDATED | Better path found; carries the 32-byte public key of the updated contact |
| 0x82 | PUSH_CODE_SEND_CONFIRMED | Delivery ACK from remote; carries ACK hash (4 bytes) + trip time (4 bytes) |
| 0x83 | PUSH_CODE_MSG_WAITING | Offline messages queued |
| 0x85 | PUSH_CODE_LOGIN_SUCCESS | Repeater/room login succeeded |
| 0x86 | PUSH_CODE_LOGIN_FAIL | Repeater/room login failed |
| 0x87 | PUSH_CODE_STATUS_RESPONSE | Repeater status response |
| 0x88 | PUSH_CODE_LOG_RX_DATA | Radio RX data with SNR (int8, units 1/4 dB), RSSI, and raw radio packet |
| 0x89 | PUSH_CODE_TRACE_DATA | Path trace result |
| 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered |
| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data |
| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response |
| 0x8E | PUSH_CODE_CONTROL_DATA | Control data push (e.g. zero-hop discovery response) |
## Data Models
### Contact
32-byte public key (primary identity), name, type (chat/repeater/room/sensor), flags, path data, GPS coordinates, last-seen timestamp. Parsed from 148-byte firmware frames with this layout:
```
[0] = resp_code
[132] = public key (32 bytes)
[33] = type (1=chat, 2=repeater, 3=room, 4=sensor)
[34] = flags (bit 0 = favorite)
[35] = path_length
[3699] = path (64 bytes)
[100131] = name (32 bytes, null-padded)
[132135] = timestamp (uint32 LE)
[136139] = latitude (int32 LE, × 1e-6 degrees)
[140143] = longitude (int32 LE, × 1e-6 degrees)
[144147] = last_modified (uint32 LE)
```
### Message (Direct)
Sender key, text, timestamp, outgoing flag, status (pending/sent/delivered/failed), message ID (UUID), retry count, ACK hash, trip time, path data, reactions.
### Channel Message
Sender name, text, timestamp, status (pending/sent/failed), repeater hops, path variants, channel index, reactions, reply threading fields.
### Channel
Index (07), name, 16-byte PSK, unread count. PSK derivation methods for hashtag (SHA-256) and community (HMAC-SHA256) channels.
### Community
UUID, name, 32-byte secret, hashtag channel list. Shared via QR code.
## Persistence
All data is stored via `SharedPreferences` (JSON-serialized). No SQLite or other database.
| Data | Storage Key Pattern | Scope |
|---|---|---|
| Contacts | `contacts<pubKey10>` | Per device identity |
| Messages | `messages_<pubKey10><contactKey>` | Per device + contact |
| Channel Messages | `channel_messages_<pubKey10><index>` | Per device + channel |
| Channels | `channels<pubKey10>` | Per device identity |
| Channel Order | `channel_order_<pubKey10>` | Per device identity |
| Contact Groups | `contact_groups<pubKey10>` | Per device identity |
| Communities | `communities_v1<pubKey10>` | Per device identity |
| Unread Counts | `contact_unread_count<pubKey10>` | Per device identity |
| Discovered Contacts | `discovered_contacts` | Global |
| App Settings | `app_settings` | Global |
| Path History | `path_history_<contactKey>` | Per contact |
## Auto-Add Configuration Bitmask
Used by `CMD_SET_AUTO_ADD_CONFIG` (58) and `RESP_CODE_AUTO_ADD_CONFIG` (25):
| Bit | Flag | Description |
|-----|------|-------------|
| 0 | 0x01 | Overwrite oldest contact when list is full |
| 1 | 0x02 | Auto-add chat users |
| 2 | 0x04 | Auto-add repeaters |
| 3 | 0x08 | Auto-add room servers |
| 4 | 0x10 | Auto-add sensors |
## Radio Packet Payload Types
Seen inside `PUSH_CODE_LOG_RX_DATA` raw packets:
| Code | Type |
|------|------|
| 0x00 | REQ (request) |
| 0x01 | RESPONSE |
| 0x02 | TXTMSG (text message) |
| 0x03 | ACK |
| 0x04 | ADVERT |
| 0x05 | GRPTXT (group/channel text) |
| 0x06 | GRPDATA (group data) |
| 0x07 | ANONREQ (anonymous request) |
| 0x08 | PATH |
| 0x09 | TRACE |
| 0x0A | MULTIPART |
| 0x0B | CONTROL |
| 0x0F | RAW_CUSTOM |
## State Management
Uses Flutter `Provider` with `ChangeNotifier`. The central state holder is `MeshCoreConnector`, which owns all in-memory collections and fires debounced (50ms) `notifyListeners()` to update the UI. In-memory conversations are windowed to 200 messages per contact; older messages remain on disk and are loaded on demand.
### Data Flow
1. Raw frames arrive over BLE/USB/TCP
2. First byte is parsed as response/push code
3. Appropriate model factory (`fromFrame()`) parses the data
4. In-memory collections are updated
5. Storage stores are persisted (async)
6. `notifyListeners()` triggers UI rebuilds
7. Screens read current state via getters
+154
View File
@@ -0,0 +1,154 @@
# Channels
## Overview
Channels are broadcast group-chat spaces secured by a 16-byte pre-shared key (PSK). Any device with the same channel index and PSK will receive and decrypt channel messages. Unlike direct messages, channel messages are broadcast to the entire mesh.
The number of active channels is determined by the firmware (default 40); the device reports its actual limit at login.
## How to Access
QuickSwitchBar tab 1 (middle) from any main screen.
## Channel Types
| Type | Icon | Color | Description |
|---|---|---|---|
| Public | Globe | Green | Fixed well-known PSK; any device can join |
| Hashtag | Hash tag | Blue | PSK derived from the hashtag name via SHA-256; discoverable by convention |
| Private | Lock | Blue | Random PSK; requires out-of-band sharing of the 32-hex key |
| Community | Groups/Tag | Magenta | PSK derived via HMAC-SHA256 from a community's shared secret |
## Channels List Screen
### What the User Sees
- **Search bar** with live text filtering (300ms debounce)
- **Sort/filter button**
- **Scrollable list of channel cards**, each showing:
- Type icon with color coding (magenta badge overlay for community channels)
- Channel name (or "Channel N" if unnamed)
- Unread badge (if messages are unread)
- Drag handle (when manual sort is active)
- **"+" FAB** to add a new channel
- **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.
Pull-to-refresh (swipe down) forces a re-fetch of channels from the device firmware.
### Sorting Options
- **Manual** (default): Drag-and-drop reordering, persisted (drag handles are hidden when a search query is active)
- **AZ**: Alphabetical
- **Latest messages**: Most recent first
- **Unread**: Most unread first
## Adding a Channel
Tap the "+" FAB to open a dialog with six options:
1. **Create Private Channel** — Enter a name (max 31 characters); a random PSK is generated
2. **Join Private Channel** — Enter a name and a 32-hex PSK (non-hex characters like spaces and dashes are silently stripped, so pasted keys with formatting are accepted)
3. **Join Public Channel** — One tap; uses the well-known public PSK (only shown if no public channel exists)
4. **Join Hashtag Channel** — Enter a hashtag name; PSK is derived from the name. If communities exist, choose between regular hashtag (SHA-256) or community hashtag (HMAC)
5. **Scan Community QR** — Opens QR scanner to join a community
6. **Create Community** — Enter a name; generates a random 32-byte secret; optionally adds a community public channel; shows QR code for sharing
## Channel Actions (Long-Press / Right-Click)
| Action | Description |
|---|---|
| Edit | Change name, PSK (with a dice icon to generate a random PSK), 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 |
| Delete | Remove the channel from the device (confirmation required) |
## Channel Chat
Tap a channel card to open the channel chat screen.
### App Bar
- Type icon: globe for public channels, tag (#) for all other channel types
- Channel name
- Subtitle: "{Public|Private} • {N} unread" (e.g., "Public • 3 unread")
### Message Display
- Reverse-scrolling list (newest at bottom)
- **Incoming messages**: Colored avatar with sender's initial (or first emoji if name starts with one; color is deterministic from sender name hash), sender name in primary color, message bubble
- **Outgoing messages**: Primary container color bubble with a small status icon: pending (clock), sent (checkmark), or failed (red error circle)
- Automatic older-message loading on scroll-to-top
- Jump-to-bottom button when scrolled up
- **Pinch-to-zoom**: Two-finger zoom (0.8x1.8x) and double-tap to reset text size
- **Message tracing mode** (when enabled in App Settings): Each bubble additionally shows path prefix bytes (`via XX,YY,...`), a timestamp, and a repeat count icon
### Message Types in Chat
- **Plain text** with linkified URLs
- **GIFs** (`g:{gifId}`) rendered inline via Giphy CDN
- **Location pins** (`m:{lat},{lon}|{label}|`) shown as tappable location cards
- **Reactions** displayed as emoji pills below target messages
### Replies (Channel Chat Only)
- **Mobile**: Swipe an **incoming** message left to trigger reply (with haptic feedback). You cannot swipe your own outgoing messages. Swipe reply is not available on desktop.
- **All platforms**: Long-press → "Reply"
- Reply banner appears above the input bar with the quoted message (tap X to cancel)
- Sent replies are prefixed `@[{senderName}] {text}`
- Received replies show a bordered quote block inside the bubble; tapping scrolls to the original. Reply previews render GIF thumbnails and location pin icons, not just text.
### Message Path Viewing
- **All platforms**: Long-press (or right-click on desktop) a message bubble → "Path"
- Opens the Channel Message Path Screen (see [Additional Features](additional-features.md))
### Context Actions (Long-Press / Right-Click)
| Action | Availability | Description |
|---|---|---|
| Reply | All messages | Triggers reply mode |
| Path | All messages | Opens message path view |
| Add Reaction | Incoming messages only | Opens emoji picker (cannot react to your own messages) |
| Copy | All messages | Copies text to clipboard |
| Mark as Unread | Incoming messages only | Marks this message and all subsequent incoming messages as unread |
| Delete | All messages | Removes locally (not from mesh) |
## Communities
Communities are a layer above channels that provide a private namespace.
### What is a Community?
A community has a name and a 32-byte random secret. Channel PSKs are derived from this secret:
- **Public channel**: `HMAC-SHA256(secret, "channel:v1:__public__")[:16]`
- **Hashtag channel**: `HMAC-SHA256(secret, "channel:v1:{hashtag}")[:16]`
Outsiders who don't know the secret cannot discover or join community channels.
### Sharing a Community
Communities are shared via QR codes containing a JSON payload:
```json
{"v": 1, "type": "meshcore_community", "name": "...", "k": "<base64url-secret>"}
```
### Managing Communities
From the channels screen overflow menu → "Manage Communities". Opens a draggable scrollable sheet (resizable 3090% of screen height):
- Each community shows its name and a short community ID (first 8 hex characters)
- **Tap a community** to directly show its QR code for sharing
- **Popup menu** per community:
- **Show QR** — displays the QR code for sharing with new members
- **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
| Aspect | Channels | Direct Messages |
|---|---|---|
| Addressing | Broadcast to all nodes with matching PSK | Point-to-point to a specific contact |
| Encryption | Shared PSK (symmetric) | Contact's public key (asymmetric) |
| Sender identity | Plain text prefix in payload | Verified via public key |
| Replies | Supported (swipe or long-press) | Not supported |
| Retry mechanism | No automatic retry | Exponential backoff with path rotation |
+121
View File
@@ -0,0 +1,121 @@
# Chat & Messaging
## Overview
The app supports two chat modes:
- **Direct messages**: Encrypted point-to-point messages to individual contacts
- **Channel messages**: Broadcast messages to shared channels (see [Channels](channels.md))
This page covers direct messaging. For channel chat, see the Channels documentation.
## How to Access
From the Contacts screen, tap any Chat-type contact to open the ChatScreen.
## Chat Screen Layout
### App Bar
- **Title**: Contact name
- **Subtitle**: Current routing path label (e.g., "2 hops", "flood (auto)", "direct (forced)") and unread count. Tapping the subtitle shows the full path details.
- **Action button**:
- **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.
### Message List
- Scrollable list with newest messages at the bottom
- **Outgoing messages**: Right-aligned, primary color background. **Failed messages** change to a red-toned error container background
- **Incoming messages**: Left-aligned, grey background with a colored avatar (initial letter or first emoji of sender name; color is deterministic from a hash of the sender name)
- Bubble width capped at 72% of screen width
- Hyperlinks rendered as tappable green underlined text
- **Pinch-to-zoom**: Two-finger zoom (0.8x1.8x) and double-tap to reset
- **Jump to bottom**: Floating button appears when scrolled away from the bottom
- **Lazy loading**: Scrolling to top loads older messages from storage
### Input Bar
- **GIF button** (left): Opens GIF picker bottom sheet
- **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
- **Send button** (right): Submits the message
- On desktop: Enter/Numpad Enter also submits
- When a GIF is selected, the text field shows an inline GIF preview with a dismiss button
## Message Types
| Type | Wire Format | Display |
|---|---|---|
| Plain text | Raw UTF-8 string | Inline text with link detection |
| GIF | `g:<giphy-id>` | Inline GIF image from Giphy CDN |
| Location pin | `m:<lat>,<lon>\|<label>\|...` | Location icon + label; tap to open map |
| Reaction | `r:<hash>:<emoji-index>` | Applied to target message as emoji pill |
## Message Status
Outgoing messages display a status indicator:
| Status | Icon | Meaning |
|---|---|---|
| Pending | Grey double-check | Queued, waiting for device to transmit (visually identical to Sent) |
| Sent | Grey double-check | Device confirmed transmission (visually identical to Pending) |
| Delivered | Green double-check | Remote node acknowledged receipt |
| Failed | Red X | All retries exhausted |
### Message Tracing Mode
When enabled in App Settings, additional metadata appears inside each bubble:
- Timestamp (HH:MM)
- Retry count (e.g., "Retry 2 of 4") — only shown for outgoing messages where at least one retry has occurred
- Status icon (outgoing only)
- Round-trip time in seconds (if delivered)
## Message Length Limits
- **Direct messages**: 156 bytes (UTF-8) — enforced in real-time by the input formatter
- **Channel messages**: 160 minus sender name length minus 2 bytes for the `"<name>: "` prefix
- Over-length paste shows a snackbar error
## Send Queue
Only one message per contact can be in-flight at a time (to avoid overflowing the firmware's 8-entry ACK table). If you send multiple messages rapidly, they are queued and sent sequentially — each waits for the previous one to be delivered, fail, or exhaust retries before transmitting.
## Retry Mechanism
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
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 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...)
5. **Max retries**: Configurable (default 5, range 210)
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"
7. If **Clear Path on Max Retry** is enabled (App Settings), the contact's stored routing path is automatically cleared when max retries are exhausted
8. **Auto route rotation**: When enabled (and no manual path override is set), the retry service uses a diversity window to avoid re-using recently tried paths, cycling through known routes on each attempt
### Manual Retry
Long-press a failed message → "Retry" to re-send using the current routing settings.
## Reactions
Add emoji reactions to incoming messages (not your own):
1. Long-press (or right-click on desktop) a message
2. Select "Add reaction" from the context menu
3. Choose from quick emojis (thumbs up, heart, laugh, party, clap, fire) or browse the full emoji picker
4. Reactions appear as pills below the message bubble with emoji and count
5. Pending reactions show at 50% opacity with a spinner
6. Failed reactions show a red retry icon (tap to retry)
## Context Actions (Long-Press / Right-Click)
| Action | Availability | Description |
|---|---|---|
| Add reaction | Incoming messages only | Opens emoji picker |
| View path | All platforms: long-press/right-click menu | Shows message routing path |
| 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) |
| Retry | Failed outgoing messages | Re-sends the message |
| Open chat with sender | Room server chats | Opens 1:1 chat with the message sender |
+118
View File
@@ -0,0 +1,118 @@
# Contacts
## Overview
The Contacts screen is the primary hub for managing mesh nodes your radio has a relationship with. A "contact" is any node whose cryptographic advertisement has been received — it can be a chat user, repeater, room server, or sensor.
## How to Access
- QuickSwitchBar tab 0 (leftmost) from Channels or Map screens (Channels is shown first after connecting)
- Back navigation from Chat or Settings screens
## Contact Types
| Type | Avatar Color | Icon | Description |
|---|---|---|---|
| Chat | Blue | Initials / emoji | Another user's mesh radio |
| Repeater | Amber | Cell tower | A mesh repeater/relay node |
| Room | Magenta | Meeting room | A room server for group chat |
| Sensor | Teal | Sensors | A sensor device |
## Contact List
Each contact is displayed as a list tile showing:
- **Avatar**: Color-coded circle with type icon (or first emoji of the contact's name if it starts with one)
- **Name**: Contact name (single line)
- **Path label**: "Direct", "N hops", or "Flood" (with forced variants if a path override is active)
- **Public key**: Shortened hex format `<XXXXXXXX...XXXXXXXX>`
- **Unread badge**: Red pill with count (if unread messages exist)
- **Last seen**: Relative timestamp ("Now", "5 mins ago", "2 hours ago", "3 days ago"). For chat contacts, this shows whichever is more recent: the last advertisement time or the last message time
- **Favorite star**: Amber star icon if favorited
- **Location pin**: Grey pin icon if the contact has GPS coordinates
Pull-to-refresh re-fetches the full contact list from the device.
## Search and Filter
A toolbar at the top provides:
**Search**: Matches contact name (case-insensitive) or public key hex prefix. Debounced at 300ms.
**Sort options**:
- Latest Messages (by most recent message)
- Heard Recently (by last seen / last message)
- AZ (alphabetical)
**Filter options**:
- All, Favorites, Users, Repeaters, Room Servers, Unread Only
## Contact Groups
Groups are a client-side organizational feature for grouping contacts.
- **Create a group**: Tap the group dropdown → "+" icon → enter name → select members → Save
- **Edit a group**: Group dropdown → pencil icon next to the group
- **Delete a group**: Group dropdown → trash icon next to the group
- **Filter by group**: Select a group from the dropdown to show only its members
Groups are stored per radio identity (scoped by public key).
**Validation rules**: Group names cannot be empty, cannot be "all" (reserved, case-insensitive), and must be unique (case-insensitive). The group creation dialog includes a built-in search field to filter contacts when selecting members. Creating a new group automatically selects it as the active filter.
## Tap Actions
| Contact Type | Action on Tap |
|---|---|
| Chat / Sensor | Opens ChatScreen for direct messaging |
| Repeater | Shows password login dialog → opens RepeaterHubScreen |
| Room | Shows password login dialog → opens ChatScreen for room chat |
## Long-Press / Right-Click Menu
| Action | Availability | Description |
|---|---|---|
| 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 |
| Room Login | Rooms only | Login dialog → ChatScreen |
| Room Management | Rooms only | Login dialog → RepeaterHubScreen (management mode) |
| Add/Remove Favorite | All types | Toggles the favorite flag |
| 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 |
| Delete Contact | All types | Confirmation dialog → removes from device and clears messages |
## App Bar Menus
The Contacts screen has a single **three-dot overflow menu** (`⋮`) in the app bar:
- 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
- Flood Advert — broadcasts across the full mesh network
- Copy Advert to Clipboard — copies your `meshcore://<hex>` URI for sharing externally
- *(divider)*
- Disconnect — disconnects from the device
- 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
### 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).
### Import from Clipboard
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
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
Contacts are shared using the `meshcore://` URI scheme:
```
meshcore://<hex-encoded-advertisement-packet>
```
This contains the node's public key and metadata. Paste it into another MeshCore app to import.
+193
View File
@@ -0,0 +1,193 @@
# Map & Location
## Overview
The Map feature is a full-featured node-location visualization and radio-planning tool built on OpenStreetMap tiles. It is one of the three primary views accessible from the QuickSwitchBar.
## How to Access
- **QuickSwitchBar tab 2** (rightmost) from Contacts or Channels
- **Deep-link from a chat message**: Tapping a shared location pin in a chat opens the map centered on that pin
- **Settings → Offline Map Cache**: Opens the tile cache management screen
## What the Map Displays
### Self Location (Teal Circle)
Your own node's position, obtained from the device firmware. Displayed as a teal `person_pin_circle` icon. Only appears if the device has GPS data or a manually-set location.
### Contact / Node Markers (Color-Coded)
All contacts with known GPS coordinates are plotted:
| Type | Color | Icon |
|---|---|---|
| Chat user | Blue | Person |
| Repeater | Green | Router |
| Room | Purple | Meeting room |
| Sensor | Orange | Sensors |
Node name labels appear automatically at zoom level 14 and above.
### Shared Map Pins (Flag Icons)
Location pins shared in chat messages are displayed as flags:
- **Blue flag**: From a direct message
- **Purple flag**: From a private channel
- **Orange flag**: From a public channel
Tap a pin to see its info. Options to "Hide" (session only) or "Remove" (persistent).
### 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 markers with a `not_listed_location` icon and a muted grey or colored border, visually distinct from confirmed-location markers.
#### Why guessed locations exist
In a mesh network, every message hops through one or more repeaters on its way to the destination. Each repeater in the path is identified by the first byte of its public key. If any of those repeaters have a known GPS location (because they advertise it), then a contact that routes through those repeaters must be somewhere within radio range of them. By combining the positions of multiple repeaters a contact is known to use, the app can triangulate a rough area where the contact is likely located.
#### How the algorithm works
1. **Build a repeater index**: The app collects all known contacts of type Repeater that have a valid GPS position and indexes them by the first byte of their public key.
2. **Collect anchor points**: For each contact that lacks GPS, the app looks at the **last-hop byte** of the contact's current path and also searches the `PathHistoryService` for recent paths. Each last-hop byte that matches a located repeater becomes an "anchor point" — a GPS coordinate the contact is likely near.
3. **Resolve ambiguity**: If multiple repeaters share the same first public-key byte (a hash collision), that byte is discarded as ambiguous. Only unambiguous one-to-one matches are kept.
4. **Filter geometric inconsistencies**: Two anchor points separated by more than `2 × maxRangeKm` (the estimated LoRa radio range, computed from the current frequency, bandwidth, spreading factor, and TX power using a free-space path loss model) cannot both be in range of the same node. Outlier anchors are removed to keep only a geometrically consistent set.
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.
- **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**:
- **High confidence** (2+ anchors): The marker border uses the node's type color (brighter border).
- **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.
#### How to read guessed locations on the map
- **Marker with `not_listed_location` icon**: This is a guessed position, not a confirmed GPS fix.
- **Colored border** (type color): Higher confidence — the contact was seen through 2 or more repeaters with known positions.
- **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.
- Guessed locations can be toggled on/off in the map filter dialog (FAB → "Guessed locations" toggle).
## Map Interactions
### Zoom and Pan
Standard pinch-to-zoom (range 218). Initial camera position is calculated from the statistical spread of all plotted points.
### Tap on a Node Marker
Opens a dialog showing: type, path (hop chain), coordinates, last-seen time, and public key. Action buttons vary by type:
- **Chat nodes**: "Open Chat"
- **Repeaters**: "Manage Repeater"
- **Rooms**: "Join Room"
### Long-Press on Empty Map Area
Shows a bottom sheet with:
- **Share marker here**: Prompts for a label, then pick a DM contact or channel to send the location to. Wire format: `m:<lat>,<lon>|<label>|poi`
- **Set as my location**: Updates your device's advertised location
### Filter Dialog (FAB)
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:
- **Key prefix filter**: Show only contacts whose public key starts with a given prefix
- **Last-seen time slider**: Exponential scale from near-zero to 6 months, with "all time" at the top end
### Legend Card (Top-Right)
Shows node count and pin count. Tappable to expand a legend of all marker types.
---
## Path Trace Map
### How to Access
- From the main map's radar icon
- From a contact's long-press menu → "Path Trace / Ping"
- From a message's path view → radar icon
### What the User Sees
A map with a polyline showing the route from your node through repeater hops to the target:
- **Green circles**: Hops with known GPS coordinates
- **Orange circles** (`~HH`): Inferred positions (no GPS but deducible from contacts)
- **Red endpoint**: Target contact with known GPS
- **Magenta endpoint**: Target with guessed position
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
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.
---
## Line-of-Sight (LOS) Analysis
### How to Access
From the main map, tap the terrain/antenna icon.
### What the User Sees
A full-screen map with a draggable bottom sheet containing:
- **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 summary**: Clear (green), Marginal (amber, within 5 m of obstruction), or Blocked (red) with distance and clearance/obstruction amount
- **Options section** (collapsible): Node toggles, endpoint dropdowns, antenna height sliders (0400 ft), Run LOS button
### Key Interactions
- **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
- **Antenna heights** are adjustable for both endpoints
- **Map line** between endpoints is colored green (clear), amber (marginal), or red (blocked)
- 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
---
## Offline Map Cache
### How to Access
Settings → App Settings → Map Display → Offline Map Cache
### What the User Sees
- Map with a blue polygon overlay showing previously selected cache bounds
- Bounding box coordinates card
- **Cache Area** controls: "Use Current View" and Clear buttons
- **Zoom Range** range slider (318, dual-handle for min and max) with estimated tile count
- **Download progress** bar (when downloading)
- **Download Tiles** and **Clear Cache** buttons
### Key Interactions
1. Pan/zoom the map to the desired area
2. Tap "Use Current View" to capture the viewport as cache bounds
3. Adjust the zoom range slider
4. Tap "Download Tiles" (confirmation dialog shows estimated count)
5. Tiles are downloaded with up to 8 concurrent connections
6. Once cached, tiles are served from disk without internet (365-day stale period)
---
## GPX Export
### How to Access
Settings → Export section
### What It Does
Exports contacts with GPS coordinates to a `.gpx` file via the OS share sheet. Three export options:
- **Export Repeaters**: Repeater and Room contacts with locations
- **Export Contacts**: Chat contacts with locations
- **Export All**: All contacts with locations
Each waypoint includes: name, lat/lon, type label, and public key hex.
---
## Location Data Sources
The phone's own GPS is **never used**. All location data comes from the mesh:
1. **Device self-location**: Read from firmware device-info response. Set manually in Settings → Location, or updated automatically if the device has a GPS module.
2. **Remote node locations**: Extracted from advertisement packets received over the mesh. Encoded as integer lat/lon × 1,000,000.
+72
View File
@@ -0,0 +1,72 @@
# Navigation
## App Flow
The app follows this general flow:
```
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**.
## Quick Switch Bar
The QuickSwitchBar is a Material 3 `NavigationBar` with a frosted-glass visual treatment (blur backdrop, transparent theme, rounded corners). It appears at the bottom of all three main screens.
| Index | Icon | Label | Screen |
|---|---|---|---|
| 0 | People | Contacts | ContactsScreen |
| 1 | Tag | Channels | ChannelsScreen |
| 2 | Map | Map | MapScreen |
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.
## Disconnection
- The disconnect button (available in the overflow menu of each main screen) 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)
- This auto-navigation behavior (`DisconnectNavigationMixin`) is shared across all main screens
## Theme and Locale
- **Theme mode** is user-configurable in App Settings (System / Light / Dark) — not locked to system
- **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)
## Full Navigation Graph
```
ScannerScreen (root, always on stack)
├─ [BLE connect] → push → ChannelsScreen
├─ [TCP icon button] → push → TcpScreen
│ └─ [TCP connected] → pushReplacement → ChannelsScreen
└─ [USB icon button] → push → UsbScreen
└─ [USB connected] → pushReplacement → ChannelsScreen
ContactsScreen (selected=0)
├─ [quick-switch 1] → pushReplacement → ChannelsScreen
├─ [quick-switch 2] → pushReplacement → MapScreen
├─ [tap contact] → push → ChatScreen
├─ [overflow > Settings] → push → SettingsScreen
└─ [overflow > Discovered] → push → DiscoveryScreen
ChannelsScreen (selected=1)
├─ [quick-switch 0] → pushReplacement → ContactsScreen
├─ [quick-switch 2] → pushReplacement → MapScreen
├─ [tap channel] → push → ChannelChatScreen
└─ [overflow > Settings] → push → SettingsScreen
MapScreen (selected=2)
├─ [quick-switch 0] → pushReplacement → ContactsScreen
├─ [quick-switch 1] → pushReplacement → ChannelsScreen
├─ [radar menu item] → enters in-map path trace mode (push → PathTraceMapScreen after path is built)
├─ [terrain menu item] → push → LineOfSightMapScreen
└─ [long-press] → share marker sheet
Settings (push from any main screen)
└─ [App Settings] → push → AppSettingsScreen
└─ [Offline Map Cache] → push → MapCacheScreen
```
Any disconnection from any screen triggers `popUntil(route.isFirst)`, returning to the Scanner.
+92
View File
@@ -0,0 +1,92 @@
# Notifications
## Overview
MeshCore Open provides both **system notifications** (push-style OS alerts) and **in-app unread badges** to inform users of new activity.
## Notification Types
### 1. Direct Message Notifications
- **Triggered when**: A new incoming message arrives from a Chat or Room contact
- **Title**: Contact's name
- **Body**: Message text (reactions show "Reacted [emoji]", GIFs show "Sent a GIF")
- **Priority**: High
- **Android channel**: `messages`
### 2. Channel Message Notifications
- **Triggered when**: A new message arrives on a non-muted channel
- **Title**: Channel name (or "Channel N" if unnamed)
- **Body**: `"<senderName>: <message text>"`
- **Priority**: High
- **Android channel**: `channel_messages`
### 3. Advertisement Notifications
- **Triggered when**: A new node is discovered on the mesh for the first time
- **Title**: "New [type] discovered" (e.g., "New Chat discovered")
- **Body**: Contact's name
- **Priority**: Default
- **Android channel**: `adverts`
### 4. Background Service Notification (Android Only)
- A persistent low-priority notification: "MeshCore running — Keeping BLE connected"
- Required by Android for foreground services to keep BLE alive in the background
- Tap to re-launch the app
- **Does not auto-start on reboot** — the user must re-open the app manually after a phone restart
### Notification Tap Behavior
Tapping a notification currently re-launches the app at the root route. It does **not** navigate directly to the relevant chat or channel.
## In-App Unread Badges
Red numeric badges appear throughout the UI:
- **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
- **Chat screen subtitle**: Shows unread count inline
- Badges cap at "9999+" for display
### How Unread Counts Work
- Stored per contact (by public key) and per channel, **scoped to the connected device's identity** (first 10 hex characters of its public key). Switching between different radios gives each its own independent unread state
- **Suppressed when viewing**: Opening a chat resets the count to 0 and cancels the OS notification
- **Ignored for**: Outgoing messages, CLI messages, and repeater contacts
- Debounced writes (500ms) to avoid excessive storage I/O during message bursts
## Notification Settings
Access via **App Settings → Notifications**:
| Setting | Default | Description |
|---|---|---|
| Enable Notifications | On | Master toggle; requests OS permission when turned on |
| Message Notifications | On | DM alerts (greyed out if master is off) |
| Channel Message Notifications | On | Channel alerts (greyed out if master is off) |
| Advertisement Notifications | On | New node alerts (greyed out if master is off) |
### Per-Channel Muting
Long-press a channel in the channels list → "Mute channel" / "Unmute channel". Muted channels do not generate OS notifications.
There is no per-contact muting.
## Rate Limiting
The notification system prevents notification storms:
- **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`). 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
- **Opening a contact chat**: Cancels the OS notification and resets unread count
- **Opening a channel**: Cancels the channel notification and resets unread count
- **Opening Contacts screen**: Cancels all advertisement notifications
## Platform Support
| Platform | Message Notifs | Badge | Background Service |
|---|---|---|---|
| Android | Yes | Via notification number | Yes (foreground service) |
| iOS | Yes | Yes (app badge) | No |
| macOS | Yes | Yes | No |
| Windows | Yes | No | No |
| Linux | Yes (if D-Bus available) | No | No |
+221
View File
@@ -0,0 +1,221 @@
# Repeater Management
## Overview
Repeater Management provides tools for administering MeshCore repeater and room server nodes. It includes device status monitoring, CLI access, telemetry reading, neighbor discovery, and remote configuration.
## How to Access
From the Contacts screen:
1. Long-press a **Repeater** or **Room** contact
2. Select "Manage Repeater" or "Room Management"
3. Enter the admin password in the login dialog
4. Navigate to the Repeater Hub Screen
### Login Dialog
- 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
- 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 (explicit failure response) stops immediately — only timeouts trigger retries
- If auto-clock-sync is enabled for this repeater (configured in Repeater Settings), a `clock sync` command is sent automatically on successful login
---
## Repeater Hub Screen
The central management screen showing:
- **Header card**: Repeater name, short public key, path label, GPS coordinates (if known)
- **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" (admin) or "Repeater Guest" / "Room Guest" (guest) based on contact type and login result:
| Card | Destination | Visibility |
|---|---|---|
| Status | Repeater Status Screen | All users |
| Telemetry | Telemetry Screen | All users |
| Neighbors | Neighbors Screen | All users |
| CLI | Repeater CLI Screen | Admin only |
| Settings | Repeater Settings Screen | Admin only |
The battery chemistry selector and CLI/Settings cards are hidden from guest users.
---
## Repeater Status
### What the User Sees
Three information cards:
**System Information**:
- Battery percentage and voltage (e.g. "85% / 3.95V"), using the battery chemistry set in the hub screen
- Clock at login time
- Uptime (days/hours/minutes/seconds)
- Queue length
- Debug flags (error event count)
**Radio Statistics**:
- Last RSSI and SNR
- Noise floor
- TX airtime and RX airtime
**Packet Statistics**:
- Packets sent and received, each broken down by flood vs. direct
- Duplicates, broken down by flood vs. direct
- Channel utilization (% of uptime used by TX + RX)
### Key Interactions
- Auto-queries the repeater on open; shows a loading spinner until data arrives
- On timeout: red snackbar error. On success: data appears in-place (no extra snackbar)
- 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)
- Accepts both binary `RESP_CODE_STATUS_RESPONSE` frames and legacy JSON text responses
---
## Repeater CLI
A terminal-style interface for sending commands directly to the repeater.
### What the User Sees
- **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
- **Input bar**: Up/down history arrows, monospace text field with `> ` prefix, send button
### Key Interactions
- Type a command and press send (or Enter on desktop)
- Up/down arrows navigate through command history
- Quick-command buttons populate and send common commands
- 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
- Clear icon: Wipes the command/response history
- Failed/timed-out commands are automatically retried once
### Available CLI Commands
The in-app help reference (help icon) documents all known commands. Categories:
**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`
**Logging**: `log start`, `log stop`, `log erase`
**Neighbors**: `neighbors`, `neighbor.remove`
**Power Management**: `get pwrmgt.support`, `get pwrmgt.source`, `get pwrmgt.bootreason`, `get pwrmgt.bootmv`
**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`
---
## Telemetry
### What the User Sees
A list of Cayenne LPP sensor channel cards:
- **Channel 1** (special): Battery voltage (shown as percentage or raw mV) and MCU temperature
- **Other channels**: Raw sensor values with appropriate labels
Shows "No data" until a response arrives from the repeater.
### Key Interactions
- Auto-queries on open
- Pull-to-refresh
- Temperature respects metric/imperial setting
- Battery readings are stored for the repeater's battery snapshot
---
## Neighbors
### What the User Sees
A card titled "Repeater's Neighbors - N" listing each neighbor as:
- Repeater name (or hex key prefix if unknown)
- Time since last heard
- SNR quality icon with color coding and label
### Key Interactions
- Auto-queries up to 15 neighbors on open
- Matches public key prefixes against known contacts to show names
- Pull-to-refresh
---
## Repeater Settings
### What the User Sees
Nine configuration cards, each with its own per-field refresh button(s):
**1. Basic Settings**
- Name field
- Admin password field (write-only; always sent when non-empty)
- Guest password field (write-only; always sent when non-empty)
**2. Radio Settings**
- Frequency (MHz)
- TX Power (dBm) — has its own independent refresh button
- Bandwidth dropdown (kHz)
- Spreading Factor (SF5SF12)
- Coding Rate (4/54/8)
- RX Gain boost toggle
**3. Location Settings**
- Latitude and longitude fields, each with an independent refresh button
**4. Features**
- Packet forwarding toggle (`set repeat`)
- 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. Network Health**
- Loop detection dropdown (off / minimal / moderate / strict; `set loop.detect`)
- Duty cycle slider (1100%; `set dutycycle`)
**6. Advertisement Settings**
- Local advert interval slider (60240 minutes) with enable/disable toggle
- Flood advert interval slider (3168 hours) with enable/disable toggle
- Flood max hops slider (064; `set flood.max`)
**7. Owner Info**
- Multi-line text field for operator contact info (`set owner.info`); newlines sent as `|`
**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 (02; `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 (0240s 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
- **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, RX Gain, latitude, longitude, and advanced fields each have independent inline refresh buttons
- 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; firmware responses are checked and partial failures are reported in a snackbar
- 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)
- **Erase Filesystem** does NOT send any command over the air — tapping it only shows a snackbar explaining the operation requires physical serial access
+79
View File
@@ -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)
+126
View File
@@ -0,0 +1,126 @@
# Scanner & Connection
## BLE Scanner (Home Screen)
The BLE Scanner is the app's home screen, displayed immediately on launch.
### How to Access
- Opens automatically when the app starts
- Returns here when disconnecting from any device
- Accessible by navigating back from a connected session
### What the User Sees
**App Bar**: Centered title "Scanner".
**Bluetooth-Off Warning Banner** (conditional): Appears when the Bluetooth adapter is off, showing a `bluetooth_disabled` icon, a warning message, and on Android, an "Enable Bluetooth" button.
**Status Bar**: A full-width colored strip reflecting the current connection state:
| State | Text | Color |
|---|---|---|
| Disconnected | "Not connected" | Grey |
| Scanning | "Scanning..." | Blue |
| Connecting | "Connecting..." | Orange |
| Connected | "Connected to \<device name\>" | Green |
| Disconnecting | "Disconnecting..." | Orange |
**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.
**App Bar Actions**: Icon buttons in the top-right corner of the app bar:
- **USB** icon button - Opens USB connection screen (Android, Windows, Linux, macOS, Chrome web only)
- **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
### Device Tile
Each discovered device is displayed as a list tile showing:
- **Signal strength icon** (color-coded by RSSI):
- Green: >= -60 dBm (excellent)
- Light green: -60 to -70 dBm (good)
- Amber: -70 to -80 dBm (fair)
- Orange: -80 to -90 dBm (weak)
- Red: < -90 dBm (poor)
- **RSSI value** in dBm (e.g., "-72 dBm")
- **Device name** (falls back to "Unknown Device")
- **Device ID** (BLE MAC address on Android; a system-assigned UUID on iOS/macOS)
- **Connect button** (the entire tile row is also tappable — both trigger connection)
Note: The weak (-80 to -90 dBm) and poor (< -90 dBm) tiers share the same icon shape and are only differentiated by color (orange vs. red).
### How Scanning Works
- 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
- Scans for 10 seconds then auto-stops
- On iOS/macOS, waits for BLE adapter initialization before starting
- If Bluetooth is turned off during a scan, scanning stops immediately
### Connecting to a Device
Tap a device tile or its Connect button:
1. The connector stops scanning and transitions to "connecting"
2. Connects to the device with a 15-second timeout (6 seconds on Linux)
3. Requests MTU 185 bytes for optimal throughput
4. Discovers BLE services and locates the Nordic UART Service
5. Subscribes to TX notifications for receiving data
6. On success, automatically navigates to the Channels screen
7. On failure, shows a red error snackbar
---
## USB Connection
### How to Access
From the Scanner screen, tap the **USB** icon button in the app bar.
### What the User Sees
- A colored status bar at the top (same color scheme as BLE scanner)
- A list of detected USB serial ports, each showing:
- Friendly display name
- Raw port name (subtitle, only shown when it differs from the display name)
- Chevron trailing icon (the entire tile is tappable to connect)
- 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
- 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
- Tap a port tile to connect
- On successful connection, navigates to Channels screen
- 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)
---
## TCP Connection
### How to Access
From the Scanner screen, tap the **TCP/IP** icon button in the app bar.
### What the User Sees
- A colored status bar at the top
- **Host address** text field
- **Port number** text field
- **Connect** button
- Transport switcher buttons (outlined, not FABs) to switch to USB or BLE
### Key Interactions
- Last-used host and port are pre-populated from saved settings
- Tap Connect to validate inputs and connect
- Host must not be empty
- Port must be a number between 1 and 65535
- Validation errors are shown as red snackbars
- 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")
- 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")
- Error messages for timeout, unsupported platform, and connection failures
+201
View File
@@ -0,0 +1,201 @@
# Settings
## How to Access
- From the Device Screen: tap the tune/sliders icon in the app bar
- From Contacts or Channels: overflow menu (three-dot) → Settings
Settings are only accessible while a device is connected.
## Settings Screen Layout
The settings screen is a scrollable list of cards:
1. [Device Info](#device-info)
2. [Node Settings](#node-settings)
3. [Location](#location)
4. [App Settings](#app-settings) (link to sub-screen)
5. [Actions](#actions)
6. [Export](#export)
7. [Debug](#debug)
8. [About](#about)
---
## Device Info
A collapsible card showing read-only device information. **Collapsed by default** — tap the header to expand with an animated chevron indicator:
| Field | Description |
|---|---|
| Name | Connected device's display name |
| ID | Device identifier |
| Status | Connected / Disconnected |
| Battery | Percentage or voltage (tap to toggle) |
| Node Name | The node's mesh identity name |
| Public Key | First 16 hex characters + "..." |
| Contacts Count | Number of known contacts |
| Channel Count | Number of configured channels |
Battery shows an alert icon and orange text when at 15% or below. The toggle only works when millivolt data is available from the firmware.
---
## Node Settings
These settings are sent directly to the connected device firmware.
### Node Name
- Opens a dialog with a text field (max 31 characters)
- Sends the new name to the device
- Confirmed via snackbar
### Radio Settings
Opens a dialog pre-populated with the device's current radio settings. Contains:
- **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 3002500 MHz
- **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz)
- **Spreading Factor**: SF5SF12
- **Coding Rate**: 4/5, 4/6, 4/7, 4/8
- **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
### 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
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
- If GPS-capable hardware (detected via `gps` custom variable):
- GPS Update Interval (seconds, 6086399, default 900 = 15 minutes). Validated and sent separately before lat/lon
- Enable GPS toggle (takes effect immediately, not deferred to Save)
- Validation: lat ±90, lon ±180
### Contact Settings
Five toggles controlling which node types are auto-added when heard:
- Auto-add Chat Users
- Auto-add Repeaters
- Auto-add Room Servers
- Auto-add Sensors
- Overwrite Oldest (when contact list is full)
### Privacy
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 (110, default 5, integer steps)
- Initial Route Weight (0.55.0, default 3.0)
- Success Increment (0.12.0, default 0.5, 0.1 steps)
- Failure Decrement (0.12.0, default 0.2, 0.1 steps)
- Max Message Retries (210, 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
---
## Actions
One-tap device operations:
| Action | Description |
|---|---|
| Sync Time | Sends current Unix timestamp to the device |
| Refresh Contacts | Re-requests the full contact list |
| 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.
---
## Debug
Two log viewers accessible via list tiles:
### BLE Debug Log
Two views (togglable via segmented button):
- **Frames view**: Direction icon, description, hex preview, timestamp per frame. Long-press to copy hex.
- **Raw Log RX view**: Decoded LoRa packets with route type, payload type, path, and summary.
- Copy-all and Clear buttons in the app bar.
### App Debug Log
Structured log entries (Info / Warning / Error), with tag, message, and timestamp.
- Must be enabled first in App Settings → Debug
- Copy-all and Clear buttons
---
## About
Shows the standard Flutter about dialog with app name, version, and legal notice.
+16
View File
@@ -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>
-2
View File
@@ -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>
+4 -1
View File
@@ -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 -124
View File
@@ -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
+50 -21
View File
@@ -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 &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<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"
+5 -2
View File
@@ -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)
}
} }
+42 -19
View File
@@ -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>
File diff suppressed because it is too large Load Diff
@@ -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 ---
+197 -88
View File
@@ -1,6 +1,9 @@
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';
// Buffer Reader - sequential binary data reader with pointer tracking // Buffer Reader - sequential binary data reader with pointer tracking
class BufferReader { class BufferReader {
int _pointer = 0; int _pointer = 0;
@@ -37,16 +40,6 @@ class BufferReader {
Uint8List readRemainingBytes() => readBytes(remaining); Uint8List readRemainingBytes() => readBytes(remaining);
String readString() {
_lastPointer = _pointer;
final value = readRemainingBytes();
try {
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
} catch (e) {
return String.fromCharCodes(value); // Latin-1 fallback
}
}
String readCStringGreedy(int maxLength) { String readCStringGreedy(int maxLength) {
_lastPointer = _pointer; _lastPointer = _pointer;
final value = <int>[]; final value = <int>[];
@@ -62,11 +55,12 @@ class BufferReader {
} }
} }
String readCString(int maxLength) { String readCString({int maxLength = -1}) {
final backupPointer = _pointer; final backupPointer = _pointer;
final value = <int>[]; final value = <int>[];
int counter = 0; int counter = 0;
while (counter < maxLength) { final maxLen = maxLength >= 0 ? maxLength : remaining;
while (counter < maxLen) {
final byte = readByte(); final byte = readByte();
if (byte == 0) break; if (byte == 0) break;
value.add(byte); value.add(byte);
@@ -209,17 +203,22 @@ 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 cmdGetTelemetryReq = 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;
const int txtTypeCliData = 1; const int txtTypeCliData = 1;
const int txtTypeSigned = 2;
// Repeater request types (for server requests) // Repeater request types (for server requests)
const int reqTypeGetStatus = 0x01; const int reqTypeGetStatus = 0x01;
@@ -228,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;
@@ -251,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;
@@ -265,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;
@@ -272,6 +289,10 @@ const int advTypeRepeater = 2;
const int advTypeRoom = 3; const int advTypeRoom = 3;
const int advTypeSensor = 4; const int advTypeSensor = 4;
const int teleModeDeny = 0;
const int teleModeAllowFlags = 1; // use contact.flags
const int teleModeAllowAll = 2;
// Payload Types // Payload Types
const int payloadTypeREQ = const int payloadTypeREQ =
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) 0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
@@ -310,11 +331,12 @@ const int autoAddSensorFlag =
// Sizes // Sizes
const int pubKeySize = 32; const int pubKeySize = 32;
const int signatureSize = 64;
const int maxPathSize = 64; 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 =
@@ -352,6 +374,9 @@ const int contactPubKeyOffset = 1;
const int contactTypeOffset = 33; const int contactTypeOffset = 33;
const int contactFlagsOffset = 34; const int contactFlagsOffset = 34;
const int contactFlagFavorite = 0x01; const int contactFlagFavorite = 0x01;
const int contactFlagTeleBase = 0x02; // 'base' permission includes battery
const int contactFlagTeleLoc = 0x04;
const int contactFlagTeleEnv = 0x08; //access environment sensors
const int contactPathLenOffset = 35; const int contactPathLenOffset = 35;
const int contactPathOffset = 36; const int contactPathOffset = 36;
const int contactNameOffset = 100; const int contactNameOffset = 100;
@@ -370,52 +395,44 @@ const int msgTextOffset = 38;
class ParsedContactText { class ParsedContactText {
final Uint8List senderPrefix; final Uint8List senderPrefix;
final String text; final String text;
const ParsedContactText({required this.senderPrefix, required this.text}); const ParsedContactText({required this.senderPrefix, required this.text});
} }
ParsedContactText? parseContactMessageText(Uint8List frame) { ParsedContactText? parseContactMessageText(Uint8List frame) {
if (frame.isEmpty) return null; if (frame.isEmpty) return null;
final code = frame[0];
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) { final message = BufferReader(frame);
try {
final code = message.readByte();
if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
return null;
}
// Companion radio layout:
// [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
if (code == respCodeContactMsgRecvV3) {
// Skip SNR and reserved bytes in v3 layout
message.skipBytes(3);
}
final senderPrefix = message.readBytes(6); // public key
message.skipBytes(1); // path length
final textType = message.readByte();
message.skipBytes(4); // timestamp (4 bytes)
final shiftedType = textType >> 2;
final isSigned = shiftedType == txtTypeSigned || textType == txtTypeSigned;
if (isSigned) {
// Signed messages have a 4-byte signature after the timestamp, before the text
message.skipBytes(4);
}
final text = message.readCString();
if (text.isEmpty) return null;
return ParsedContactText(senderPrefix: senderPrefix, text: text);
} catch (e) {
debugPrint('Error parsing contact message text: $e');
return null; return null;
} }
// Companion radio layout:
// [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
final isV3 = code == respCodeContactMsgRecvV3;
final prefixOffset = isV3 ? 4 : 1;
const prefixLen = 6;
final txtTypeOffset = prefixOffset + prefixLen + 1;
final timestampOffset = txtTypeOffset + 1;
final baseTextOffset = timestampOffset + 4;
if (frame.length <= baseTextOffset) return null;
final flags = frame[txtTypeOffset];
final shiftedType = flags >> 2;
final rawType = flags;
final isPlain = shiftedType == txtTypePlain || rawType == txtTypePlain;
final isCli = shiftedType == txtTypeCliData || rawType == txtTypeCliData;
if (!isPlain && !isCli) {
return null;
}
var text = readCString(
frame,
baseTextOffset,
frame.length - baseTextOffset,
).trim();
if (text.isEmpty && frame.length > baseTextOffset + 4) {
text = readCString(
frame,
baseTextOffset + 4,
frame.length - (baseTextOffset + 4),
).trim();
}
if (text.isEmpty) return null;
final senderPrefix = frame.sublist(prefixOffset, prefixOffset + prefixLen);
return ParsedContactText(senderPrefix: senderPrefix, text: text);
} }
// Helper to read uint32 little-endian // Helper to read uint32 little-endian
@@ -438,18 +455,9 @@ int readInt32LE(Uint8List data, int offset) {
return val; return val;
} }
// Helper to read null-terminated UTF-8 string // Helper to convert uint32 to hex string
String readCString(Uint8List data, int offset, int maxLen) { String ackHashToHex(int ackHash) {
int end = offset; return ackHash.toRadixString(16).padLeft(8, '0');
while (end < offset + maxLen && end < data.length && data[end] != 0) {
end++;
}
try {
return utf8.decode(data.sublist(offset, end), allowMalformed: true);
} catch (e) {
// Fallback to Latin-1 if UTF-8 decoding fails
return String.fromCharCodes(data.sublist(offset, end));
}
} }
// Helper to convert public key to hex string // Helper to convert public key to hex string
@@ -459,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;
@@ -509,7 +522,7 @@ Uint8List buildSendTextMsgFrame(
final writer = BufferWriter(); final writer = BufferWriter();
writer.writeByte(cmdSendTxtMsg); writer.writeByte(cmdSendTxtMsg);
writer.writeByte(txtTypePlain); writer.writeByte(txtTypePlain);
writer.writeByte(attempt.clamp(0, 3)); writer.writeByte(attempt.clamp(0, 255));
writer.writeUInt32LE(timestamp); writer.writeUInt32LE(timestamp);
writer.writeBytes(recipientPubKey.sublist(0, 6)); writer.writeBytes(recipientPubKey.sublist(0, 6));
writer.writeString(text); writer.writeString(text);
@@ -569,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();
@@ -717,25 +741,19 @@ 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);
if (lastModified != null) {
// Longitude final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
final longitude = lon ?? 0.0; writer.writeUInt32LE(lastModifiedTimestamp);
writer.writeInt32LE((longitude * 1e6).round()); }
}
if (lastModified != null) {
// Last modified
final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
writer.writeUInt32LE(lastModifiedTimestamp);
} }
return writer.toBytes(); return writer.toBytes();
@@ -838,7 +856,7 @@ Uint8List buildSendCliCommandFrame(
final writer = BufferWriter(); final writer = BufferWriter();
writer.writeByte(cmdSendTxtMsg); writer.writeByte(cmdSendTxtMsg);
writer.writeByte(txtTypeCliData); writer.writeByte(txtTypeCliData);
writer.writeByte(attempt.clamp(0, 3)); writer.writeByte(attempt.clamp(0, 255));
writer.writeUInt32LE(timestamp); writer.writeUInt32LE(timestamp);
writer.writeBytes(repeaterPubKey.sublist(0, 6)); writer.writeBytes(repeaterPubKey.sublist(0, 6));
writer.writeString(command); writer.writeString(command);
@@ -858,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}) {
@@ -937,3 +1016,33 @@ Uint8List buildSetAutoAddConfigFrame({
writer.writeByte(flags); writer.writeByte(flags);
return writer.toBytes(); return writer.toBytes();
} }
//Build CMD_SEND_TELEMETRY_REQ
// Format: [cmd][reserved x3][pub_key? x32]
Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
final writer = BufferWriter();
writer.writeByte(cmdSendTelemetryReq);
if (pubKey != null && pubKey.length == pubKeySize) {
writer.writeBytes(Uint8List(3)); // reserved bytes
writer.writeBytes(pubKey);
} else {
writer.writeBytes(Uint8List(3)); // reserved bytes
}
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]);
}
+20
View File
@@ -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",
];
}
+209 -9
View File
@@ -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();
}
} }
+19
View File
@@ -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) {
+59
View File
@@ -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();
}
+63
View File
@@ -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 '';
}
}
+38
View File
@@ -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';
}
}
+78
View File
@@ -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,
);
}
+61 -10
View File
@@ -1,8 +1,61 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.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 '../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.
static Widget buildLinkifyText({
required BuildContext context,
required String text,
required TextStyle style,
TextStyle? linkStyle,
VoidCallback? onSecondaryTap,
}) {
final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
const linkifiers = [UrlLinkifier(), EmailLinkifier()];
void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
if (PlatformInfo.isDesktop) {
final linkify = SelectableLinkify(
text: text,
style: style,
linkStyle: effectiveLinkStyle,
options: options,
linkifiers: linkifiers,
onOpen: onOpen,
);
if (onSecondaryTap == null) return linkify;
return Listener(
onPointerDown: (event) {
if (event.buttons & kSecondaryMouseButton != 0) onSecondaryTap();
},
behavior: HitTestBehavior.translucent,
child: linkify,
);
}
return Linkify(
text: text,
style: style,
linkStyle: effectiveLinkStyle,
options: options,
linkifiers: linkifiers,
onOpen: onOpen,
);
}
static Future<void> handleLinkTap(BuildContext context, String url) async { static Future<void> handleLinkTap(BuildContext context, String url) async {
// Show confirmation dialog // Show confirmation dialog
final shouldOpen = await showDialog<bool>( final shouldOpen = await showDialog<bool>(
@@ -51,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,
),
); );
} }
} }
+91
View File
@@ -0,0 +1,91 @@
import 'package:flutter/foundation.dart';
import '../connector/meshcore_protocol.dart';
import '../models/contact.dart';
class PathHelper {
static String formatPathHex(List<int> pathBytes) {
return pathBytes
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join(',');
}
static String hopHex(int byte) {
return byte.toRadixString(16).padLeft(2, '0').toUpperCase();
}
static String formatHopHex(List<int> hopBytes) {
return hopBytes
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join();
}
static String? hopName(int byte, List<Contact> allContacts) {
final matches = allContacts
.where(
(c) =>
c.publicKey.isNotEmpty &&
c.publicKey.first == byte &&
(c.type == advTypeRepeater || c.type == advTypeRoom),
)
.toList();
if (matches.isEmpty) return null;
if (matches.length == 1) return matches.first.name;
return matches.map((c) => c.name).join(' | ');
}
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 ');
}
}
+76
View File
@@ -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!);
}
}
+49
View File
@@ -8,6 +8,50 @@ class ReactionInfo {
} }
class ReactionHelper { class ReactionHelper {
/// Apply a reaction to a list of messages by matching the reaction hash.
///
/// [messages] - the message list to search
/// [reactionInfo] - the parsed reaction
/// [getTimestampSecs] - extract timestamp seconds from a message
/// [getSenderName] - extract sender name for hash (null for 1:1 implicit)
/// [getMessageText] - extract message text
/// [getReactions] - extract current reactions map
/// [shouldSkip] - filter function to skip messages (e.g., skip outgoing for incoming reactions)
/// [updateMessage] - callback to update the message at index with new reactions
///
/// Returns whether a match was found.
static bool applyReaction<T>({
required List<T> messages,
required ReactionInfo reactionInfo,
required int Function(T) getTimestampSecs,
required String? Function(T) getSenderName,
required String Function(T) getMessageText,
required Map<String, int> Function(T) getReactions,
required bool Function(T) shouldSkip,
required void Function(int index, Map<String, int> newReactions)
updateMessage,
}) {
final targetHash = reactionInfo.targetHash;
for (int i = messages.length - 1; i >= 0; i--) {
final msg = messages[i];
if (shouldSkip(msg)) continue;
final msgHash = computeReactionHash(
getTimestampSecs(msg),
getSenderName(msg),
getMessageText(msg),
);
if (msgHash == targetHash) {
final currentReactions = Map<String, int>.from(getReactions(msg));
currentReactions[reactionInfo.emoji] =
(currentReactions[reactionInfo.emoji] ?? 0) + 1;
updateMessage(i, currentReactions);
return true;
}
}
return false;
}
static List<String>? _cachedEmojis; static List<String>? _cachedEmojis;
/// Combined list of all reaction emojis in fixed order. /// Combined list of all reaction emojis in fixed order.
@@ -65,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';
}
} }
+68
View File
@@ -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,
),
);
}
+16 -3
View File
@@ -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) {
+1111 -172
View File
File diff suppressed because it is too large Load Diff
+1295 -250
View File
File diff suppressed because it is too large Load Diff
+1038 -82
View File
File diff suppressed because it is too large Load Diff
+1266 -221
View File
File diff suppressed because it is too large Load Diff
+1232 -159
View File
File diff suppressed because it is too large Load Diff
+2869
View File
File diff suppressed because it is too large Load Diff
+1313 -240
View File
File diff suppressed because it is too large Load Diff
+2965
View File
File diff suppressed because it is too large Load Diff
+2965
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1036 -97
View File
File diff suppressed because it is too large Load Diff
+1234 -257
View File
File diff suppressed because it is too large Load Diff
+969 -30
View File
File diff suppressed because it is too large Load Diff
+1006 -4
View File
File diff suppressed because it is too large Load Diff
+975 -36
View File
File diff suppressed because it is too large Load Diff
+969 -30
View File
File diff suppressed because it is too large Load Diff
+972 -33
View File
File diff suppressed because it is too large Load Diff
+1094 -175
View File
File diff suppressed because it is too large Load Diff
+951 -12
View File
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More