Compare commits

..

646 commits
Alpha2 ... main

Author SHA1 Message Date
Zach
5cfe45b953 IOS build changes 2026-04-15 09:06:17 -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
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
Some checks failed
Build / android (push) Has been cancelled
Build / ios (push) Has been cancelled
Build / linux (push) Has been cancelled
Build / macos (push) Has been cancelled
Build / web (push) Has been cancelled
Flutter and Dart / analyze (push) Has been cancelled
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
Some checks failed
Build / android (push) Has been cancelled
Build / ios (push) Has been cancelled
Build / linux (push) Has been cancelled
Build / macos (push) Has been cancelled
Build / web (push) Has been cancelled
Flutter and Dart / analyze (push) Has been cancelled
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
Some checks are pending
Build / android (push) Waiting to run
Build / ios (push) Waiting to run
Build / linux (push) Waiting to run
Build / macos (push) Waiting to run
Build / web (push) Waiting to run
Flutter and Dart / analyze (push) Waiting to run
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
Some checks failed
Build / android (push) Has been cancelled
Build / ios (push) Has been cancelled
Build / linux (push) Has been cancelled
Build / macos (push) Has been cancelled
Build / web (push) Has been cancelled
Flutter and Dart / analyze (push) Has been cancelled
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
Some checks are pending
Build / android (push) Waiting to run
Build / ios (push) Waiting to run
Build / linux (push) Waiting to run
Build / macos (push) Waiting to run
Build / web (push) Waiting to run
Flutter and Dart / analyze (push) Waiting to run
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
60e8ee0130 fix: simplify method call for writing data in UsbSerialService 2026-03-14 18:41:57 -07:00
zjs81
6dfb7a4b69 fix: auto-add flag parsing, contact cache restore, and USB reconnect
- Fix operator precedence bug in _handleAutoAddConfig where `flags &
  flag != 0` was parsed as `flags & (flag != 0)`, always checking bit 0
  instead of the correct flag bit
- Populate _contacts from cache in loadContactCache() so contacts
  persist across app restarts
- Toggle DTR low→high on USB connect to force device to see a fresh
  connection
- Add 10ms inter-frame delay for USB sends to prevent missed responses
- Deassert DTR before closing USB port on disconnect/dispose

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 18:41:21 -07:00
zjs81
28a423e0a8 fix: correct location validation and clean up target contact handling
- Fix asymmetric lat/lon validation in _handleContactAdvert (was checking
  longitude != 0 for latitude; now uses (latitude != 0 || longitude != 0)
  for both)
- Remove duplicate targetGuessed assignment in path_trace_map
- Rename public target field to private _targetContact, use local variable
  to avoid unnecessary null-aware operators

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 18:14:39 -07:00
Winston Lowe
3593cfa843
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 18:10:44 -07:00
Winston Lowe
dc85e7a41c
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 18:10:17 -07:00
Winston Lowe
9265daaf16
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 18:10:09 -07:00
Winston Lowe
4b744184c2
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 18:09:54 -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
Winston Lowe
06a906f4f7 Enhance location handling and improve path trace functionality across screens 2026-03-14 17:51:24 -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
Winston Lowe
24fa78741b add TCP server address and port settings to AppSettings and update TcpScreen 2026-03-14 11:46:05 -07:00
Winston Lowe
79a45c527b Unify contact retrieval by introducing allContacts getter 2026-03-14 11:45:47 -07:00
zjs81
8b280b37be
Merge pull request #293 from zjs81/map-set-location-and-connector-improvements
feat: add set-as-my-location from map long-press, connector and UI
2026-03-14 09:55:02 -07:00
zjs81
fa4da979af feat: enhance location update feedback and improve message retry error handling 2026-03-14 09:54:50 -07:00
zjs81
91608ff09e feat: improve message matching logic and update notification IDs for advertisements 2026-03-14 09:44:37 -07:00
zjs81
71f59d23df feat: add set-as-my-location from map long-press, connector and UI improvements
Add "Set as my location" option to the map long-press bottom sheet,
allowing users to set their device position directly from the map.
Includes connector, chat, contacts, and message retry service improvements.
2026-03-14 09:33:37 -07:00
zjs81
e90742be25
Merge pull request #272 from just-stuff-tm/tcp
feat: Add TCP connection support and UI integration
2026-03-13 11:04:11 -07:00
Zach
db935a7454 refactor(tcp): promote MeshCoreTcpConnector, fix translations, harden UI
- Replace thin MeshCoreTcpManager facade with a proper MeshCoreTcpConnector
  that owns TcpTransportService and the frame subscription, mirroring
  MeshCoreUsbManager. The connector no longer holds a raw TcpTransportService
  or a _tcpFrameSubscription field.
- Remove hardcoded default host IP from TcpScreen (keep port 5000 hint).
- Disable connect button during scanning state, not just connecting state.
- Fix tcpPortLabel mistranslated as nautical "port/harbor" in de, it, pt,
  nl, sv, sk, sl, zh; fix corrupted Slovak tcpPortHint ("5 000" → "5000").
- Remove unused tcpStatus_connecting string from all 15 locale arb files
  and all generated app_localizations_*.dart files.
- Add extendedPadding to TCP screen FABs to match USB screen.
- Add Key to connect button; update tests to use byKey and assert
  onPressed == null when button is disabled during scanning.
2026-03-13 10:59:09 -07:00
Winston Lowe
1ad5db27ca
Merge branch 'main' into tcp 2026-03-12 23:22:30 -07:00
Winston Lowe
81758adc61
Dev discovery (#291)
* Refactor contact handling: replace DiscoveryContact with Contact, update related methods and settings

* Enhance contact handling: include latitude, longitude, and last modified timestamp in contact updates; refactor path handling to accommodate discovered contacts across multiple screens

* Enhance SNRIndicator: include discovered contacts in name resolution for repeaters

* Refactor path handling: replace addReturnPath with buildPath to improve path construction logic and handle target contact types

* Update lib/screens/map_screen.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add localization for "Show Discovery Contacts" in multiple languages and refactor location plausibility check in map screen

* Enhance contact management: update discovered contacts' active status and improve contact handling with flags and raw packet data

* Refactor ChannelsScreen: pass ChannelMessageStore to buildExpandedContent and ensure messages are cleared after channel creation

* Update MapScreen: adjust label zoom threshold and refactor guessed marker building to include labels

* Refactor ChannelsScreen: change channelMessageStore to a private getter and update its usage in buildExpandedContent calls

* Enhance location plausibility check: add latitude and longitude bounds to ensure valid coordinates

* Update lib/connector/meshcore_connector.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor MeshCoreConnector and related stores: update discovered contacts handling, migrate legacy keys, and set public key in community store

* Refactor MeshCoreConnector and ChannelsScreen: update discovered contacts handling and set public key in community store; enhance location plausibility check in MapScreen

* Update CMD_ADD_UPDATE_CONTACT frame format to include optional latitude and longitude fields

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 23:08:46 -07:00
Winston Lowe
c81791cf1e
Migrate legacy storage keys to scoped keys in various store classes (#289) 2026-03-12 08:39:17 -07:00
Winston Lowe
1fba5312a2
Refactor storage classes to include companion's public key (#277)
* Refactor storage classes to include public key handling and improve data loading/saving logic

* Remove redundant publicKeyHex handling from ContactDiscoveryStore and fix key reference in saveContacts method

* Remove unused app_logger import from ContactDiscoveryStore

* Add warning log for empty publicKeyHex in saveChannelMessages method

* Add warning log for empty publicKeyHex in clearMessages method

* Migrate legacy storage keys to scoped keys across multiple stores

* Remove legacy unscoped keys during migration in storage classes

* Update lib/storage/contact_store.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 00:14:48 -07:00
just-stuff-tm
2f770bbd53 fix(tcp): reset state on aborted pre-handshake connect 2026-03-10 21:38:35 -04:00
just-stuff-tm
9db79e9d40 test(tcp): harden cancel-race handling and add coverage
- tighten late TCP connect error suppression to manual-cancel disconnecting/disconnected windows
- keep TCP handshake failures surfaced outside explicit cancel flow
- allow TcpScreen connect action when connector is scanning
- add connector-level tests for late-error suppression classifier
- add TcpScreen test covering connect from scanning state
2026-03-10 20:06:05 -04:00
just-stuff-tm
1913a5aa11 fix(tcp): guard connect cancellation race and align USB screen actions
- add connectTcp cancellation guards after socket connect and connect delay so handshake does not proceed when transport/state changed
- ignore late TCP connect errors after manual cancel or transport switch to avoid spurious second disconnect paths
- keep TCP action hidden only on web and show Bluetooth action on USB screen across platforms for navigation consistency
2026-03-10 19:27:39 -04:00
just-stuff-tm
929c1c3d28 fix(tcp): cancel pending connects on disconnect and propagate remote close 2026-03-09 20:39:17 -04:00
just-stuff-tm
7a2bb20bf7 feat: Add TCP connection support and UI integration
- Implemented TCP transport service for native platforms.
- Added TCP connection screen with input fields for host and port.
- Integrated TCP connection options into the scanner and USB screens.
- Updated localization files for new TCP-related strings.
- Added tests for TCP connection flow and error handling.
- Enhanced USB screen to include TCP connection option.
- Improved layout to ensure no overflow in narrow widths for scanner and USB screens.
2026-03-07 20:07:19 -05:00
zjs81
a1b77bb29b
Merge pull request #269 from zjs81/dev-latLonFix
Changed contacts latitude and longitude fields to be null until parsed and set
2026-03-07 13:53:09 -07:00
zjs81
4eecfc92dc
Merge pull request #252 from just-stuff-tm/feature/usb
Feature/usb
2026-03-07 13:16:39 -07:00
zjs81
90c8cf5f3e Add TODO to switch flserial to official repo 2026-03-07 13:12:45 -07:00
zjs81
06fa176367 Narrow macOS sandbox entitlement to /dev/cu. and /dev/tty. only
The /dev/ prefix granted read/write to all device nodes. The app only
needs access to serial port devices (/dev/cu.* and /dev/tty.*) for USB
LoRa communication.
2026-03-07 13:10:42 -07:00
zjs81
e4285774a0 Merge branch 'main' into feature/usb 2026-03-07 13:03:15 -07:00
zjs81
b2da695102 Run dart format 2026-03-07 13:01:27 -07:00
zjs81
e1327a93c7 Fix contact sync fallback when channel 0 never arrives
On web BLE, contact sync is deferred until channel 0 arrives via
_handleChannelInfo. If channel 0 times out or channel sync completes
without it, _pendingInitialContactsSync stays true and contacts never
load. Add fallback in _cleanupChannelSync to trigger getContacts() if
the flag is still set when channel sync ends.
2026-03-07 13:00:23 -07:00
zjs81
421bc71bb7 Enhance USB port opening and reading logic with improved error handling and debug logging 2026-03-07 12:55:15 -07:00
zjs81
fef73b7b62 Refactor USB screen, add debug logging, fix UI issues
- Rewrite UsbScreen to mirror ScannerScreen patterns (status bar,
  tap-to-connect port list, bottom FABs, SnackBar errors)
- Extract MeshCoreUsbManager from MeshCoreConnector for cleaner
  USB transport ownership
- Add debug logging throughout USB connection flow (connector,
  manager, web/native services)
- Print debug logs to console in debug mode even when app debug
  log setting is disabled
- Localize remaining hardcoded strings (Web Serial Device fallback
  label, USB status bar keys, companion firmware timeout hint)
- Fix Swedish misspelling in translations (stöderliga → stödda)
- Guard Linux notification init against missing D-Bus session bus
- Fix SNRIndicator hit-test error by adding minimum size constraints
- Update USB flow tests for new UI patterns
2026-03-07 12:38:28 -07:00
Winston Lowe
84ec139ce6 Add latitude and longitude fields to contact handling in MeshCoreConnector 2026-03-07 11:02:47 -08:00
Winston Lowe
b748b96237
Enhance contact handling logic in MeshCoreConnector to support conditional addition based on auto-add settings (#268) 2026-03-07 01:45:53 -08:00
Winston Lowe
c2671ac2ae
Refactor data handling of contacts (#267)
* Refactor data handling in MeshCoreConnector and BufferReader for improved readability and efficiency

* Update lib/connector/meshcore_connector.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix pointer tracking in BufferReader's readCString method

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-07 01:23:46 -08:00
just-stuff-tm
8238b6197f Regenerated localization files 2026-03-07 01:16:04 -05:00
just_stuff_tm
435ba89982
Merge branch 'zjs81:main' into feature/usb 2026-03-06 20:41:58 -05:00
just-stuff-tm
0565cee461 Enhance message merging logic and improve USB port listing 2026-03-06 20:38:03 -05:00
just-stuff-tm
ab2b509d6a Merge branch 'main' into feature/usb 2026-03-06 20:31:05 -05:00
zjs81
eba95af31f
Merge pull request #259 from ericszimmermann/ez_shorten_lastSeen
Shorten lastSeen for en,de,es,fr
2026-03-06 18:11:26 -07:00
zjs81
04c016cfe1
Merge pull request #266 from zjs81/zjs81-patch-1
Fix formatting of cryptocurrency addresses in README
2026-03-06 15:41:21 -07:00
zjs81
ea2354712d
Fix formatting of cryptocurrency addresses in README 2026-03-06 15:41:02 -07:00
zach
7a0b8aad3d Added more crypto payment options 2026-03-06 15:39:54 -07:00
zjs81
bd34bb5e88
Merge pull request #264 from zjs81/dev-guessed-locations
Dev guessed locations
2026-03-06 15:19:03 -07:00
zach
81548fdc21 ai fixes 2026-03-06 15:18:48 -07:00
zach
b2770ef028 fix ai suggestions 2026-03-06 15:11:21 -07:00
zach
7c479f9121 Formatted 2026-03-06 15:03:12 -07:00
zach
1f2dfc555b Add guessed node location map keys and translations
Adds map_showGuessedLocations and map_guessedLocation to app_en.arb and translates them across all 14 supported locales. Regenerates dart localizations.
2026-03-06 15:02:37 -07:00
zjs81
8eb6f32fef
Merge pull request #239 from zjs81/dev-notifyListener
Implements a debounced notification listener in MeshCoreConnector
2026-03-06 14:52:27 -07:00
zjs81
d96cd34771
Merge pull request #251 from zjs81/dev-discoverScreen
Contact discovery
2026-03-06 11:59:24 -07:00
just-stuff-tm
fb58a3262c addressed codex review cleanup 2026-03-05 02:50:38 -05:00
just-stuff-tm
f584c4fba0 added linux notification service 2026-03-05 02:26:37 -05:00
just-stuff-tm
b5b930646f Update flserial dependency to a specific commit reference 2026-03-05 02:26:37 -05:00
just-stuff-tm
3452bdae8c Refactor test cases for USB flow and port labels for improved readability 2026-03-05 02:26:37 -05:00
just-stuff-tm
25fc9454a8 Add error handling tests for USB connection and listing ports 2026-03-05 02:26:37 -05:00
just_stuff_tm
524558c511 clean 2026-03-05 02:26:37 -05:00
just_stuff_tm
367e47bb1e Fix USB device name matching and correct localization strings 2026-03-05 02:26:37 -05:00
just_stuff_tm
21ff765e41 Refactor USB permission handling and reset initial channel sync flag 2026-03-05 02:26:37 -05:00
just_stuff_tm
38d40ca0a4 Enhance USB error handling and improve user feedback
- Updated the _friendlyErrorMessage method in UsbScreen to provide more user-friendly error messages based on specific PlatformException codes.
- Added localized error messages for various USB-related errors, improving clarity for users.
- Modified the UsbSerialService to rethrow exceptions instead of throwing StateError, allowing for better error propagation.
- Updated the usb_flow_test to reflect changes in the USB display label behavior, ensuring the test accurately describes the functionality.
2026-03-05 02:26:37 -05:00
just_stuff_tm
5b4535d5dc update flserial dependency reference from main to master 2026-03-05 02:26:37 -05:00
Ben Allfree
f9b6299620 gitmodule cleanup 2026-03-05 02:26:37 -05:00
just_stuff_tm
7cb84dbf6f Dart Format 2026-03-05 02:26:37 -05:00
just_stuff_tm
44c0670dae Refine USB transport flow
- replace Android USB dependency with app-owned USB host implementation\n- restore BLE-first scanner flow with USB secondary action\n- tighten Web Serial key handling and disconnect logging\n\nTODO (follow-up):\n- review non-English localization copy for tone and consistency\n- trim remaining unused/awkward localization strings introduced during USB UI changes
2026-03-05 02:26:37 -05:00
Ben Allfree
74da9e82b5 wip 2026-03-05 02:25:46 -05:00
Ben Allfree
63583dadda wip 2026-03-05 02:25:46 -05:00
Ben Allfree
32632669c3 wip 2026-03-05 02:25:46 -05:00
Ben Allfree
3c0c0d1dea wip 2026-03-05 02:25:46 -05:00
Ben Allfree
e6c9a3fea7 wip 2026-03-05 02:25:46 -05:00
just_stuff_tm
f5154b0033 Improve sender name resolution for room server messages by handling missing room-contact keys 2026-03-05 02:25:46 -05:00
just_stuff_tm
4c7ee3b3b0 Enhance USB serial services with debug logging and reset functionality
- Introduced debug logging in USB serial services for better traceability.
- Added reset method to UsbSerialFrameDecoder to clear buffered data.
- Updated tests to verify the reset functionality of the decoder.
2026-03-05 02:25:46 -05:00
just_stuff_tm
c2f544eeba I restored the Web BLE behavior in [meshcore_connector.dart] to the earlier Windows/Chrome-working state aligned with the logic that was present around commit fcef3de57837983a300634aa3e0a77622e945cc2,
What is back:
- Web BLE resets handshake state before connect
- skips `requestMtu()` on web
- retries `discoverServices()` once on the transient web disconnect case
- uses the non-blocking web `setNotifyValue(true)` workaround again
- skips the immediate `SELF_INFO` wait/refresh stack on web BLE
- defers contact loading on web BLE until after channel `0`
- uses the Web-specific bounded `SELF_INFO` retry timer
- re-enables initial channel-sync gating for web BLE
2026-03-05 02:25:05 -05:00
just_stuff_tm
98cdac4309 Refactor MeshCoreConnector to streamline connection handling and remove web-specific logic for contact synchronization... Back to the way it was before.. For some reason the fix worked on my machine but wwhen i built web from upstream it didnt work 2026-03-05 02:25:05 -05:00
just_stuff_tm
d6d11eaad2 Update active USB port key and label on connection, notify listeners 2026-03-05 02:25:05 -05:00
just_stuff_tm
3cef9e81b6 Remove unawaited background service start during USB connection initialization 2026-03-05 02:25:05 -05:00
just_stuff_tm
5216e00807 Refactor USB port handling to introduce display labels and improve state management 2026-03-05 02:25:05 -05:00
just_stuff_tm
a0feb129e1 Add post-frame callback to disconnect USB transport on dispose if not navigated to contacts 2026-03-05 02:25:05 -05:00
just_stuff_tm
f39a22668e Add initial load scheduling and tests for USB screen and frame codec functionality 2026-03-05 02:25:05 -05:00
just_stuff_tm
781090243c Enhance USB functionality by adding request port label management and platform support checks 2026-03-05 02:25:05 -05:00
just_stuff_tm
ca5784f3f8 Add post-frame callback to ensure disconnection on dispose when navigation hasn't changed 2026-03-05 02:25:05 -05:00
just_stuff_tm
dcad5c586d Refactor USB connection handling to use scheduled closure and improve error management in USB services 2026-03-05 02:25:05 -05:00
just_stuff_tm
4b24506310 Remove unused import of 'dart:typed_data' in usb_serial_service_web.dart 2026-03-05 02:25:05 -05:00
just_stuff_tm
47c4e0fb82 Fix USB permission receiver registration for compatibility with Android Tiramisu 2026-03-05 02:25:05 -05:00
just_stuff_tm
c041e05972 Improve error message for unavailable RX characteristic in USB communication 2026-03-05 02:25:05 -05:00
just_stuff_tm
612612795a Update French localization for connection choice subtitle 2026-03-05 02:25:05 -05:00
just_stuff_tm
3cec3dc233 Improve USB disconnection handling and add payload length validation for USB frames 2026-03-05 02:25:05 -05:00
just_stuff_tm
3542adad1d Update USB communication note for clarity in Swedish localization 2026-03-05 02:25:05 -05:00
just_stuff_tm
115689ad95 Improve USB connection handling by preventing connection attempts when already connected 2026-03-05 02:25:05 -05:00
just_stuff_tm
9a0572e8e4 Add payload length validation in USB frame decoder 2026-03-05 02:25:05 -05:00
just_stuff_tm
2d1160d992 Enhance BLE connection handling and improve USB connection messaging
- Wrapped BLE scan and connection methods in try-catch blocks to handle errors gracefully and provide debug output.
- Added retry logic for service discovery on web platforms after transient disconnections.
- Updated USB connection messages in multiple languages to reflect active support on Android and desktop platforms.
- Improved loading indicators for contacts screen to show a spinner during data loading.
2026-03-05 02:25:05 -05:00
just_stuff_tm
ee3af52c0f Add initial contacts sync handling for web Bluetooth transport 2026-03-05 02:25:05 -05:00
just_stuff_tm
98f7c3b088 Refactor USB handling to improve connection management and error cleanup 2026-03-05 02:25:05 -05:00
just_stuff_tm
f462815775 Refactor USB connection handling and improve notification setup 2026-03-05 02:25:05 -05:00
just_stuff_tm
5f4333398e Enhance Bluetooth scanning and notification handling for web platform 2026-03-05 02:25:05 -05:00
just_stuff_tm
c23a1da430 Add web serial support and USB tests 2026-03-05 02:25:05 -05:00
just_stuff_tm
22a53439b1 Initialize USB Supoport for Andriod and Desktop 2026-03-05 02:25:05 -05:00
Winston Lowe
7d8e049745
Enhance message parsing and error handling in MeshCoreConnector (#260)
* Enhance readString method to include Latin-1 fallback for decoding errors

* Refactor _parseContactMessage to improve error handling and message parsing logic

* Update lib/connector/meshcore_connector.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 22:56:39 -08:00
ericz
3502559fae minus to tilde 2026-03-04 22:49:20 +01:00
ericz
e125318137 Shorten lastSeen for en,de,es,fr 2026-03-04 21:41:51 +01:00
Winston Lowe
d53465d13b persist discovered contacts when all are removed 2026-03-03 17:57:56 -08:00
Winston Lowe
a0efbbe4bd Persist Discovered Contacts when updated 2026-03-03 17:44:28 -08:00
zjs81
bd5db9a9d5
Merge pull request #253 from ericszimmermann/ez_search-displayed-prefix
Allow search for prefix as Displayed in contact list
2026-03-02 18:44:04 -07:00
zjs81
79b17b53a0
Merge pull request #246 from Specter242/codex/signal-ui-consistency
Unify signal indicator UI across RSSI and SNR
2026-03-02 18:42:21 -07:00
ericz
647fe1523e make it that even combination <0x90 is allowed. 2026-03-02 21:42:44 +01:00
ericz
b7d5ee5754 Allow search for prefix as Displayed in contact list. 2026-03-02 21:35:16 +01:00
Winston Lowe
38856c67e5 feat: Add functionality to delete all discovered contacts
- Implemented a new method to remove all discovered contacts from the list.
- Added confirmation dialog for deleting all discovered contacts in the discovery screen.
- Updated localization files to include new strings for deleting all discovered contacts.
- Refactored contact import logic to streamline the process.
- Enhanced the discovery handling to notify users appropriately based on settings.
2026-03-02 10:23:14 -08:00
zjs81
6bd3c17cdf
Merge pull request #217 from MeshEnvy/chrome/main
enh: Chrome compatibility
2026-03-01 20:02:29 -07:00
zjs81
6d0712c450
Merge pull request #240 from ericszimmermann/ez_removeDevicenameBrackets
Show name of connected companion
2026-03-01 19:48:54 -07:00
Winston Lowe
ddeb1edc2e refactor(discovery): simplify sorting logic for last seen contacts 2026-03-01 14:40:26 -08:00
Winston Lowe
8d73602509 add flags for manual contact addition and telemetry mode handling 2026-03-01 14:36:04 -08:00
Winston Lowe
fcab69f9f0 refactor(connector): adjust frame length check and simplify contact handling logic
refactor(settings): extract settings sending logic into a separate method
refactor(ble_debug_log_service): remove unused command case for radio settings
refactor(app_bar): update compact width threshold for app bar display
2026-03-01 13:05:57 -08:00
Winston Lowe
d2640e1294 feat(localization): update 'overwrite oldest contact' subtitle for multiple languages 2026-03-01 10:52:19 -08:00
Winston Lowe
b02225c02e refactor(connector): remove unused radio settings frame and update command constant 2026-03-01 10:41:31 -08:00
Winston Lowe
128e99e3e7 refactor(settings): remove unused import for adaptive_app_bar_title 2026-03-01 10:35:32 -08:00
Winston Lowe
12bf46bba1 feat(localization): update contact settings translations for multiple languages
- Translated contact settings and related strings in Slovenian, Swedish, Ukrainian, Chinese, Dutch, Polish, Portuguese, Russian, and Slovak.
- Added new strings for discovered contacts actions such as adding, copying, and deleting contacts.
- Enhanced the DiscoveryContact model to include a rawPacket field for better data handling.
- Updated the contacts screen to support new actions in the context menu for discovered contacts.
- Improved the contact discovery store to handle the serialization of the new rawPacket field.
2026-03-01 10:13:17 -08:00
Winston Lowe
92d8e7cd0b Refactor contact search functionality to use DiscoveryContact model and simplify query matching 2026-02-28 19:14:22 -08:00
Winston Lowe
75610695c2 Add contact settings and discovery features
- Implemented contact settings in localization files for Swedish, Ukrainian, and Chinese.
- Added new DiscoveryContact model to handle discovered contacts.
- Created DiscoveryScreen to display discovered contacts with filtering and sorting options.
- Integrated contact discovery storage to persist discovered contacts.
- Updated settings screen to include options for automatic contact addition.
- Enhanced app bar and list filter widgets for better user experience.
- Fixed variable naming inconsistencies in contact model.
2026-02-28 19:11:11 -08:00
Specter242
57ea30cae9 Unify signal indicator UI 2026-02-27 14:30:15 -05:00
Winston Lowe
e139383335
Add localized search functionality for contacts (#244)
- Introduced new localization keys for searching contacts, users, favorites, repeaters, and room servers in multiple languages.
- Updated localization files for Italian, Bulgarian, German, English, Spanish, French, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese.
- Enhanced the contacts screen to dynamically display search hints based on the selected contact type filter.
- Modified the map screen to utilize the new search functionality for contacts without a number.
2026-02-26 22:53:52 -08:00
ZIER
64428294c9 info • Unnecessary use of string interpolation • lib/widgets/app_bar.dart:43:23 • unnecessary_string_interpolations 2026-02-26 08:59:58 +01:00
ZIER
e7a8c36bc4 more aesthetically pleasing display of Companionname 2026-02-26 08:51:57 +01:00
Winston Lowe
2a62390903 Implement debounced notification listener updates in MeshCoreConnector 2026-02-25 21:58:35 -08:00
Ben Allfree
75d25f6312 Merge branch 'main' into chrome/main 2026-02-24 22:51:51 -08:00
Ben Allfree
2a3119544c Merge branch 'main' of github.com:MeshEnvy/meshcore-open 2026-02-24 22:50:20 -08:00
Ben Allfree
fb41a5bf10
Merge branch 'zjs81:main' into main 2026-02-24 22:47:48 -08:00
Ben Allfree
d88786bb0f ble filtering 2026-02-24 22:41:03 -08:00
Ben Allfree
e3148dd449 Merge main into chrome/main 2026-02-24 21:17:33 -08:00
Ben Allfree
96371c03ae pub lock upate 2026-02-24 21:17:24 -08:00
Ben Allfree
cac65face6 Merge main into chrome/main 2026-02-24 21:15:49 -08:00
zjs81
bdb1eb6b42
Merge pull request #179 from MGJ520/main
Correct Chinese translation
2026-02-24 20:26:54 -07:00
zjs81
f2ccec2926 Merge branch 'main' into MGJ520/main
# Conflicts:
#	lib/l10n/app_zh.arb
2026-02-24 20:21:10 -07:00
zjs81
31671958d5
Merge pull request #234 from ericszimmermann/favorite_filter_ez
favorites handling only
2026-02-24 20:15:05 -07:00
zjs81
ea379ce50b Fix dart format line length in contacts_screen.dart 2026-02-24 20:11:56 -07:00
zjs81
50af2e0bc9 Fix review issues: dedicated l10n keys, remove unrelated CI/macOS changes, translate all locales
- Replace concatenated favorite toggle strings with dedicated listFilter_addToFavorites/removeFromFavorites keys
- Remove unrelated CI artifact upload step from build.yml
- Revert unrelated macOS GeneratedPluginRegistrant.swift change
- Add comment explaining groups hidden under favorites filter
- Translate new keys across all 14 locales
2026-02-24 20:07:15 -07:00
zjs81
d5ac84430c Merge branch 'main' into favorite_filter_ez 2026-02-24 19:51:02 -07:00
zjs81
190fd3b353 Remove pubspec.lock from version control 2026-02-24 19:44:15 -07:00
zjs81
a2d1cb2a99 add pubspec.lock to .gitignore 2026-02-24 19:42:12 -07:00
zjs81
83386a8cde
Merge pull request #214 from MeshEnvy/fix/cursor-focus
enh: return cursor focus to message window after send
2026-02-24 19:37:43 -07:00
zjs81
acc0fff2dc
Merge pull request #215 from MeshEnvy/fix/enter-send-giphy
enh <enter> to send giphy
2026-02-24 19:13:55 -07:00
ericz
a26055c93f resolved analyte code failure: unused import 2026-02-25 00:49:41 +01:00
ericz
5a70ed48cf favorites handling only 2026-02-24 23:56:30 +01:00
Ben Allfree
a777236cd9
Merge branch 'zjs81:main' into main 2026-02-24 13:26:23 -08:00
zjs81
a42cf77a70
Merge pull request #232 from just-stuff-tm/PR-Combined-228-220-219-201
Pr combined #228 #220 #219 #201
2026-02-24 13:20:02 -07:00
just_stuff_tm
31db565ebf PR Combined #228 #220 #219 #201 2026-02-24 13:20:39 -05:00
just_stuff_tm
515b9c1f29 fix los init localization 2026-02-24 12:51:58 -05:00
just_stuff_tm
ea1d728d4f Merge remote-tracking branch 'origin/issue-fix-channel-edit-delete-actions' into combined-prs 2026-02-24 12:45:51 -05:00
just_stuff_tm
86bde1d178 Merge remote-tracking branch 'origin/los-elevation-icon' into combined-prs 2026-02-24 12:45:47 -05:00
just_stuff_tm
de63733bb9 Merge remote-tracking branch 'origin/calculate-refrac-los' into combined-prs 2026-02-24 12:40:26 -05:00
just_stuff_tm
c880c2d107 fix channel actions context 2026-02-24 00:02:10 -05:00
just_stuff_tm
2a7cc28a3a fix 2026-02-23 23:46:25 -05:00
just_stuff_tm
8a16024642 fix(chat): stabilize pinch-to-zoom scaling 2026-02-23 23:06:27 -05:00
just_stuff_tm
0f17e2382c feat(chat): add global pinch-to-zoom text scaling via ChatTextScaleService 2026-02-23 22:41:32 -05:00
just_stuff_tm
6065059241 fix: keep los panel reactive 2026-02-23 19:35:51 -05:00
just_stuff_tm
faefef14ff fix: restore baseline freq in los text 2026-02-23 19:29:36 -05:00
just_stuff_tm
ddc87f3a27 chore: remove translation script 2026-02-23 19:14:00 -05:00
just_stuff_tm
2188b49726 fix: refresh los localization 2026-02-23 19:06:52 -05:00
just_stuff_tm
1a9b7b0d55 chore: remove 0.15 text 2026-02-23 18:18:02 -05:00
just_stuff_tm
74e29a6c0f fix: clamp los profile bounds 2026-02-23 18:12:04 -05:00
Ben Allfree
7740698cde Merge branch 'main' into chrome/main 2026-02-23 15:03:20 -08:00
Ben Allfree
972ae809e3 Merge branch 'main' into fix/cursor-focus 2026-02-23 14:58:03 -08:00
Ben Allfree
deb46553f3 Merge remote-tracking branch 'origin/main' into fix/enter-send-giphy 2026-02-23 14:57:28 -08:00
Ben Allfree
58fc55df13 Merge remote-tracking branch 'upstream/main' 2026-02-23 14:56:00 -08:00
just_stuff_tm
ea2f35ec2e fix: keep los metadata on failure 2026-02-23 15:59:18 -05:00
just_stuff_tm
e2585c0992 fix: reduce rebuilds in los panel 2026-02-23 15:44:21 -05:00
just_stuff_tm
78f1a7b28e fix: normalize stored frequency 2026-02-23 15:12:32 -05:00
just_stuff_tm
0121b5f649
Merge branch 'zjs81:main' into issue-fix-channel-edit-delete-actions 2026-02-23 14:47:48 -05:00
just_stuff_tm
ec14870aed Update after upstream merged other commits 2026-02-23 14:42:30 -05:00
just_stuff_tm
c0516a475d fix: extend los profile edges 2026-02-23 14:36:10 -05:00
just_stuff_tm
b998186430
Merge branch 'main' into los-elevation-icon 2026-02-23 12:19:57 -05:00
just_stuff_tm
16b2c24983 Propagate LOS frequency data and clamp bounds 2026-02-23 12:18:42 -05:00
just_stuff_tm
c8ff0cc943 Merge upstream/main 2026-02-23 12:14:38 -05:00
zjs81
64bf307d09
Merge pull request #216 from MeshEnvy/feat/hide-message-tracing
feat: hide message tracing
2026-02-23 07:16:24 -07:00
Ben Allfree
88f8066ed3 code formatting 2026-02-23 04:53:01 -08:00
Ben Allfree
c8f93f9902 code cleanup 2026-02-23 04:30:13 -08:00
Ben Allfree
c34be44950 merge from chat trace 2026-02-23 04:25:04 -08:00
Ben Allfree
bf5fadd15e revert lockfile 2026-02-23 04:13:52 -08:00
Ben Allfree
3730b2a6c2 formatting 2026-02-23 04:13:38 -08:00
Ben Allfree
173fdf7168 chat fixes 2026-02-23 04:11:46 -08:00
Ben Allfree
549fc62632 chat fixes 2026-02-23 04:09:27 -08:00
Ben Allfree
53d073d8f2 deprecation fix 2026-02-23 03:43:49 -08:00
Ben Allfree
7465e81996 add done_all icon 2026-02-23 03:31:01 -08:00
just_stuff_tm
677b25908a Document LOS frequency and k-factor math
Show the connector frequency right next to the Frequency label, display the derived k value, and keep the info dialog tied to the exact
2026-02-23 03:11:14 -05:00
just_stuff_tm
fc55fb98ce Document LOS frequency and k-factor math
Show the connector frequency right next to the Frequency label, display the derived k value, and keep the info dialog tied to the exact
2026-02-23 02:48:28 -05:00
just_stuff_tm
2bdd9d35cc feat: show radio horizon on los profile 2026-02-23 02:02:17 -05:00
just_stuff_tm
1f816f7e08 ran dart format . on libs/icons/los_icon.dart 2026-02-23 01:06:25 -05:00
just_stuff_tm
bd27c90216 feat: render los elevation via material symbol 2026-02-23 00:54:51 -05:00
just_stuff_tm
9bcb8b9ca6 feat: render los elevation via svg 2026-02-23 00:36:49 -05:00
just_stuff_tm
aaf79c90c9 feat: show los elevation icon 2026-02-23 00:01:13 -05:00
just_stuff_tm
08edd2696e Revert "feat: add custom los icon"
This reverts commit 0f2d18d6fa.
2026-02-22 23:47:49 -05:00
just_stuff_tm
0f2d18d6fa feat: add custom los icon 2026-02-22 23:39:52 -05:00
just_stuff_tm
298951f8bc bring up to current main 2026-02-22 18:43:37 -05:00
just_stuff_tm
f3db63ceea
Delete pubspec.lock 2026-02-22 17:37:58 -05:00
just_stuff_tm
47044ae14e fix(l10n): add channels_channelDeleteFailed with proper placeholder typing and translations 2026-02-22 17:37:10 -05:00
just_stuff_tm
f4dd76a459
Delete .local-agent/memory.local.md 2026-02-22 16:07:32 -05:00
just_stuff_tm
ab76a52d71
Delete .local-agent/AGENTS.local.md 2026-02-22 16:07:19 -05:00
just_stuff_tm
332bb5ef3a Updated PR and Added snackbar Translations 2026-02-22 16:06:08 -05:00
just_stuff_tm
81a423d096
Merge branch 'main' into issue-fix-channel-edit-delete-actions 2026-02-22 15:49:51 -05:00
zjs81
700e85b13d
Merge pull request #208 from Specter242/codex/java17-wakelock-alignment
Align Android app module to Java 17 and bump wakelock_plus
2026-02-22 13:10:49 -07:00
zjs81
9a27953a6e
Merge pull request #196 from zjs81/fix-channel-del
clear app db of channel messages on delete
2026-02-22 13:10:05 -07:00
just_stuff_tm
abde4a5e46
Merge branch 'zjs81:main' into issue-fix-channel-edit-delete-actions 2026-02-22 15:06:58 -05:00
zjs81
6e1cb0482f
Merge branch 'main' into fix-channel-del 2026-02-22 13:01:36 -07:00
zjs81
c28b38a233
Merge pull request #210 from spfmoby/better-french-translations
Better french translations
2026-02-22 12:58:58 -07:00
zjs81
722caf774e
Merge pull request #211 from MeshEnvy/chrome/1-readme
docs: add chrome support notice
2026-02-22 12:56:43 -07:00
Ben Allfree
4975b5366e formatting fixes 2026-02-22 11:34:37 -08:00
Ben Allfree
d269e181c3 formatting fix 2026-02-22 11:34:18 -08:00
Ben Allfree
35498c1b90 formatting fix 2026-02-22 11:31:56 -08:00
Ben Allfree
bf4f52a4e3 hide message tracing 2026-02-22 11:27:32 -08:00
Ben Allfree
c284e571b0 hide message tracing 2026-02-22 11:27:06 -08:00
Ben Allfree
a1ee0789a6 deploy on tag only 2026-02-22 11:04:54 -08:00
Ben Allfree
3ca53e967c fix: <enter> to send giphy 2026-02-22 10:51:19 -08:00
Ben Allfree
096e0a4184 fix: return cursor to message window after send 2026-02-22 10:49:28 -08:00
Ben Allfree
40ac95e8e6 wrangler deploy 2026-02-22 10:48:22 -08:00
Ben Allfree
377f1df445 fix: browser detection 2026-02-22 10:47:51 -08:00
Ben Allfree
9865a03c53 fix: <enter> to send giphy 2026-02-22 09:20:20 -08:00
Ben Allfree
a5555bd606 fix: return cursor to message window after send 2026-02-22 09:16:07 -08:00
Ben Allfree
1b4d31a36e gitignore update 2026-02-22 09:11:49 -08:00
Ben Allfree
8e07440114 BLE fix 2026-02-22 08:38:22 -08:00
Ben Allfree
71129bdf4d chrome BLE load fix 2026-02-22 08:37:07 -08:00
Ben Allfree
ab05cf8b3e chrome BLE sync 2026-02-22 08:33:45 -08:00
Ben Allfree
452e5337f0 chrome connect 2026-02-22 08:31:29 -08:00
Ben Allfree
6ac987e7cf select BLE device 2026-02-22 08:10:16 -08:00
Ben Allfree
5522f9a236 BLE select cancel 2026-02-22 08:05:19 -08:00
Ben Allfree
b4f79c1aae Merge branch 'enh/filter-ble-at-os' into chrome/main 2026-02-22 07:41:36 -08:00
Ben Allfree
b08defcff4 Merge branch 'chrome/4-chrome-required-screen' into chrome/main 2026-02-22 07:40:57 -08:00
Ben Allfree
5676cbd84e chrome required screen 2026-02-22 07:40:40 -08:00
Ben Allfree
cf8f01128b filter BLE at OS level 2026-02-22 07:15:09 -08:00
Ben Allfree
b5e47ce44f filter BLE at OS level 2026-02-22 07:09:35 -08:00
Ben Allfree
7b2f75047c Merge branch 'chrome/1-readme' into chrome/main 2026-02-22 06:59:05 -08:00
Ben Allfree
6d63e49938 add platforminfo helper 2026-02-22 06:54:27 -08:00
Ben Allfree
c7b33f1d1b readme update 2026-02-22 06:51:40 -08:00
Ben Allfree
7288f11c88 add chrome in planning 2026-02-22 06:49:14 -08:00
spfmoby
2306269384 Better french translations 2026-02-22 15:20:55 +01:00
just_stuff_tm
41ff2353a4
Merge branch 'main' into issue-fix-channel-edit-delete-actions 2026-02-22 06:40:04 -05:00
Krasimir Kazakov
b3ad54f296
Added mute channel functionality (#209) 2026-02-21 23:51:48 -08:00
Leah
7cb4c5a334
Swipe to reply (#160)
* Add swipe to reply

* format

* Cleaned up code

* format

* remove my gitignore change - ignore this

* fix

* Update lib/screens/channel_chat_screen.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update lib/screens/channel_chat_screen.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor onHorizontalDragStart for readability

fixed formating.

* Fix swipe end handling in channel chat screen

* Refactor swipe gesture handling in chat screen

* Update lib/screens/channel_chat_screen.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update lib/screens/channel_chat_screen.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor swipe handling for reply functionality

* Adjust swipe thresholds and logic in chat screen

* Conditionally render reply bubble or padding

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Winston Lowe <wel97459@gmail.com>
2026-02-21 23:44:20 -08:00
just_stuff_tm
bb8ad70cb9
Merge branch 'zjs81:main' into issue-fix-channel-edit-delete-actions 2026-02-22 00:04:28 -05:00
Specter242
8fe4129204 Align Android app module to Java 17 and bump wakelock_plus 2026-02-21 21:01:57 -05:00
Aaron Easterling
2feff809ff
Mark pending channel messages sent on RESP_CODE_SENT (#186)
* Mark pending channel message sent on RESP_CODE_SENT

* Disambiguate RESP_CODE_SENT handling for direct vs channel

* Handle channel sent feedback when firmware returns RESP_CODE_OK

* Correlate channel OK ACKs and queue reaction channel sends
2026-02-21 15:31:51 -08:00
just_stuff_tm
51d70ce086
fix(appbar): prevent title overflow on narrow widths (#205)
Use width-aware layout in AppBarTitle to avoid RenderFlex overflows under tight title constraints and larger text scaling. Hide subtitle and signal indicators progressively when space is limited while preserving normal behavior on wider layouts.
2026-02-21 15:20:56 -08:00
Winston Lowe
b05b62eeee
Changed all map lables to look the same across all map ui (#206)
* Refactor label display in Line Of Sight and Map screens for improved alignment and styling

* Refactor label positioning and styling in ChannelMessagePathMap and PathTraceMap screens for improved alignment
2026-02-21 14:55:42 -08:00
just_stuff_tm
061b715694
Fix repeater battery % inconsistency and add configurable repeater battery chemistry (#199)
* fix(repeater): unify battery percentage math and add repeater chemistry setting

- Add shared battery percent utility used by connector, repeater status, and telemetry

- Add repeater-specific battery chemistry persistence and service accessors

- Add repeater chemistry selector in Repeater Hub

- Ensure telemetry and status compute percentages consistently from same chemistry

- Add focused battery utility tests

Refs #116

Refs #174

* fix: Flutter Analyzer Errors fixed Recent Merge Compatible

* Unify repeater battery source across status and telemetry
2026-02-21 14:54:39 -08:00
just_stuff_tm
f38b8b0319
Merge branch 'zjs81:main' into issue-fix-channel-edit-delete-actions 2026-02-21 12:54:06 -05:00
Winston Lowe
304c389669
Refactor label display in Line Of Sight and Map screens for improved alignment and styling (#204) 2026-02-20 23:41:20 -08:00
Winston Lowe
7acfe47fd7
Refactor map legend and filtering logic for contacts with location, to show count of active markers. (#203) 2026-02-20 22:09:11 -08:00
just_stuff_tm
f4b18d97a1
Added Line Of Sight Feature for repeater placement, Added app wide Units Setting (#198)
* feat: add LOS workflow, global units, l10n cleanup, and mobile UI overflow fixes

Squashes prior PR commits into one changeset including: LOS map/service/tests, global metric/imperial unit system adoption, notification/BLE safety fixes, app-wide localization backfill/mojibake cleanup, and responsive UI title/overflow hardening.

* l10n: revert unrelated locale churn for LOS feature

* feat: keep LOS with app-wide unit settings

* fix: resolve post-merge app bar/import analyzer errors

* style: format screen files for CI
2026-02-20 22:08:23 -08:00
Winston Lowe
d2b693e5ce
Add a signal readout for the nearest repeater. With improvements to app bar and other UI polish. (#200)
* Refactor Cayenne LPP parsing with error handling and logging

- Added error handling and logging to the Cayenne LPP parsing methods to manage malformed data gracefully.
- Improved the structure of the parsing logic for better readability and maintainability.
- Updated the Contact model to include error handling during frame parsing.
- Refactored Channels, Contacts, Map, and Neighbours screens to utilize a new AppBarTitle widget for consistent app bar design.
- Enhanced the BatteryIndicator widget to display SNR information for direct repeaters.
- Introduced SNRUi class for better management of SNR icon and text representation.
- Improved error handling in PathTraceMap and Neighbours screens to log errors appropriately.

* Fix trace route bytes generation logic in Contact model

* Ignore advertisements from self in MeshCoreConnector

* Refactor PathTraceData to use List<double> for snrData and adjust data mapping in PathTraceMapScreen

* Add SNRIndicator to AppBar and refactor BatteryIndicator layout

* Enhance path management dialog to display direct repeaters with color coding based on signal strength

* Remove unused import from SNR indicator widget

* Update lib/models/contact.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update lib/connector/meshcore_connector.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update lib/connector/meshcore_connector.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update lib/screens/path_trace_map.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update lib/widgets/battery_indicator.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update lib/helpers/cayenne_lpp.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor packet handling to skip only the RSSI byte for improved reliability

* Add SNR indicator localization and update UI references for nearby repeaters

* Handle loading state and error parsing in PathTraceMapScreen; update SNR indicator dialog content layout

* Throw an exception for unsupported LPP types in CayenneLpp class

* Refactor AppBarTitle widget to remove unused style parameter; update related screens to reflect changes
Improve SNR handling by adding validation for spreading factor range in snrUiFromSNR function
Update contact handling in MeshCoreConnector to fix variable naming and improve readability
Stop parsing unsupported LPP types in CayenneLpp to avoid misalignment

* Sort direct repeaters by last updated time and SNR; limit to top three for improved path management dialog

* Prevent notifications for chat and sensor adverts without a valid path

* Implement ranking system for direct repeaters based on SNR and recency; update related UI components to reflect changes

* Refactor localization keys for "neighbors" terminology across multiple languages

- Updated localization keys from "neighbours" to "neighbors" in the following files:
  - app_localizations_bg.dart
  - app_localizations_de.dart
  - app_localizations_en.dart
  - app_localizations_es.dart
  - app_localizations_fr.dart
  - app_localizations_it.dart
  - app_localizations_nl.dart
  - app_localizations_pl.dart
  - app_localizations_pt.dart
  - app_localizations_ru.dart
  - app_localizations_sk.dart
  - app_localizations_sl.dart
  - app_localizations_sv.dart
  - app_localizations_uk.dart
  - app_localizations_zh.dart
- Updated corresponding ARB files to reflect the changes in keys.
- Renamed the NeighboursScreen to NeighborsScreen in the chat and repeater hub screens for consistency.

* Adjust ranking calculation for direct repeaters by adding offset to SNR for improved accuracy

* Fix typo in variable name for second direct repeater in path management dialog

* Refactor ranking calculation for direct repeaters and update path handling in channel message screens

* Refactor path handling in ChannelMessagePathScreen to improve logic for outgoing messages and channel messages

* Fix AppBarTitle horizontal overflow with long titles (#187)

* Initial plan

* Wrap title Column in Expanded to prevent horizontal overflow in AppBarTitle

Co-authored-by: wel97459 <12990640+wel97459@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: wel97459 <12990640+wel97459@users.noreply.github.com>

* Refactor AppBarTitle widget to simplify Text widget initialization

* Add "Show All Paths" feature to chat path management

- Implemented localization for "Show All Paths" in multiple languages (DE, EN, ES, FR, IT, NL, PL, PT, RU, SK, SL, SV, UK, ZH).
- Updated path management dialog to include a toggle for showing all paths.
- Refactored path history display logic to conditionally show paths based on the toggle state.
- Cleaned up unused print statements and improved code readability in path tracing and chat screens.

* Refactor FeatureToggleRow visibility in chat and path management dialogs based on repeaters list

* Remove unused import of 'dart:ffi' in path_trace_map.dart

* Refactor repeater management logic and update UI state handling in chat and path management dialogs

* Refactor RX data handling and improve repeater management logic in chat and path management dialogs

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: wel97459 <12990640+wel97459@users.noreply.github.com>
2026-02-20 20:27:38 -08:00
just_stuff_tm
ba2763a3f6 fix(channels): make edit/delete actions use parent context after bottom sheet closes
Root cause: edit/delete dialogs were opened from the sheet context after Navigator.pop, so context.mounted was false and follow-up actions never ran.

Also keeps async await/error handling for channel edit/delete so failures surface to users instead of silently dropping.
2026-02-20 01:28:13 -05:00
Ded
0c4910e149
Merge pull request #195 from MeshEnvy/rbenv
add rbenv support
2026-02-19 12:40:27 -08:00
446564
4bf2519559 clear app db of channel messages on delete
we were only deleting channels and messages on device and the in app db would persist
this caused weird messages to later show up in other channels as they were deleted and
added due to the fact we store messages by channel index(slot #)
2026-02-19 11:46:57 -08:00
Ben Allfree
19edeab9d5 add rbenv support 2026-02-19 11:17:58 -08:00
MGJ
0e81d75cce
Merge branch 'main' into main 2026-02-19 13:07:08 +08:00
zjs81
9437846127
Merge pull request #182 from Specter242/feature/protocol-compat
Handle RESP_CODE_ERR frames explicitly in connector
2026-02-18 13:04:16 -07:00
Specter242
50ab46ed40 Remove incidental whitespace-only diff from protocol PR 2026-02-18 12:45:41 -05:00
Specter242
dc193be8ed Trim protocol PR to explicit RESP_CODE_ERR handling only 2026-02-18 12:45:02 -05:00
Specter242
8a804a3706 Remove unused protocol placeholder field and unify version source 2026-02-18 12:30:00 -05:00
Specter242
1dc90d0e89 Add device protocol version tracking and error frame handling
Port from meshcore-open: parse protocol version from byte 1 of device
info frame, expose supportsFloodScope getter (version >= 8), handle
respCodeErr frames with debug logging. Reset on disconnect.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit a29bb9cdd7a02a85af26d94dd3c787cabd124629)
2026-02-18 11:14:01 -05:00
zjs81
5f2312e086
Merge pull request #180 from zjs81/fix/radio-params-fw-compat
Refactor radio settings and localization updates
2026-02-18 00:14:32 -07:00
zjs81
4239fb11ed Fix radio settings to only send repeat byte when the current state is known 2026-02-18 00:07:08 -07:00
zjs81
5fae2e5f73 fix formatting 2026-02-17 23:50:11 -07:00
zjs81
947fafbbb7 Refactor radio settings and localization updates
fixes #72

- Removed preset configurations for 915 MHz, 868 MHz, and 433 MHz from the RadioSettings model.
- Introduced a new list of regional preset configurations for various countries.
- Updated the settings screen to use a dropdown for selecting presets instead of chips.
- Added a switch for enabling client repeat functionality with appropriate warnings for frequency usage.
- Updated localization files for multiple languages to reflect changes in settings related to client repeat functionality.
2026-02-17 23:42:04 -07:00
MGJ
a9fbf8c7f5 Correct Chinese translation 2026-02-17 13:30:23 +08:00
zjs81
72f0aa7208 Update dependencies and improve code consistency across multiple files 2026-02-14 02:22:45 -07:00
zjs81
f87d4896ab
Merge pull request #161 from ChaoticLeah/enhancement/bluetooth-disabled-warning
Add warning when bluetooth is off
2026-02-14 02:00:36 -07:00
zjs81
9250dfec31 Gate the turn on BLE button to android 2026-02-14 01:54:30 -07:00
zjs81
37db955ab2 Fixed banner flash, added enable bluetooth button fixed theme to use app theme colors removed FAB overrides because material 3 does this for us, fixed missing translations. 2026-02-14 01:46:40 -07:00
zjs81
739d9475c0 Merge remote-tracking branch 'origin/main' into enhancement/bluetooth-disabled-warning 2026-02-14 01:28:54 -07:00
zjs81
b526175be4 bump version for android 2026-02-14 01:13:06 -07:00
Winston Lowe
73081862ad
Add path tracing functionality (#165)
- Implemented path tracing feature in the map screen, allowing users to add nodes to a path and visualize it on the map.
- Added buttons for starting path tracing, removing the last node, and running the path trace.
- Introduced a new overlay to display current path information and distance.
- Updated localization files for multiple languages to include new strings related to path tracing.
- Refactored map rendering logic to accommodate path tracing visuals.
2026-02-14 01:10:34 -07:00
just_stuff_tm
fac062a100
Refine device info layout and add collapsible map legend (#164) 2026-02-12 13:46:28 -07:00
Leah
ef6bd78632
Merge branch 'enhancement/bluetooth-disabled-warning' of github.com:ChaoticLeah/meshcore-open into enhancement/bluetooth-disabled-warning 2026-02-12 20:15:58 +01:00
Leah
01c8390989
make stuff unawaited + maybe fix edge case? 2026-02-12 20:14:56 +01:00
Leah
c05f813d65
Update lib/screens/scanner_screen.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-12 20:02:56 +01:00
Ded
c52b19b09f
Merge pull request #162 from just-stuff-tm/fix/battery-layout-overflow
Fix battery chemistry dropdown layout overflow
2026-02-11 21:27:30 -08:00
just_stuff_tm
6a666839b6 Fix battery chemistry dropdown layout overflow 2026-02-12 00:05:00 -05:00
Leah
bc77f7e287
Remove unused translation
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-11 23:03:41 +01:00
Leah
9332d8126f
linted and added greying out 2026-02-11 22:58:15 +01:00
Leah
9ce00556ec
Add warning when bluetooth is off 2026-02-11 22:40:42 +01:00
Ded
4995f5f380
Merge pull request #159 from ChaoticLeah/add-flake-nix
Add flake.nix for development environment
2026-02-11 09:10:46 -08:00
Leah
4e6e7b6061
fix smaller copilot issues 2026-02-11 17:45:15 +01:00
Leah
aa350aa4ae
fixing copilot issues 2026-02-11 17:33:31 +01:00
Leah
dfd38b19e9
add flake.lock 2026-02-11 17:26:43 +01:00
Leah
4afab3f629
remove unnessisary bits and nix darwin stuff 2026-02-11 17:25:44 +01:00
Ded
67816130ac
Merge pull request #152 from zjs81/remove-wakelock
remove wakelock
2026-02-11 08:18:57 -08:00
Ded
d573f0c312
Merge pull request #158 from ChaoticLeah/update-gitignore
Update .gitignore to exclude .gradle/
2026-02-11 08:16:38 -08:00
Ded
5b699cd624
keep ignores organized 2026-02-11 08:16:07 -08:00
Leah
a4d3d248a5
Add flake.nix for development environment 2026-02-11 17:15:49 +01:00
Ded
2a3f2b3a24
Merge pull request #150 from ericszimmermann/german
translations to german updated.
2026-02-11 08:13:28 -08:00
Leah
675083fa01
Update .gitignore to exclude .gradle/ 2026-02-11 17:10:49 +01:00
Ded
5fc4b80b16
Merge pull request #144 from zjs81/support-whisperos
add support for whipseros
2026-02-11 08:03:44 -08:00
446564
84a32c1e67 remove wakelock
was being used to keep ble active which is not what it does

in early testing the ble remains connected with display off and also
when switching apps
2026-02-10 19:38:46 -08:00
ericz
607583060a translations to german updated. 2026-02-10 22:55:39 +01:00
Ded
71cf556b61
Merge pull request #148 from spfmoby/more-better-french-translations
Still better french translations
2026-02-10 11:24:47 -08:00
Zach
c26174ad18 Chore bump version 2026-02-10 09:01:56 -07:00
spfmoby
04021a39a1 Better french translations 2026-02-10 08:12:51 +01:00
446564
fe23e9f7a0 add support for whipseros
needed a new ble prefix
2026-02-09 05:36:25 -08:00
Ded
d7ec1876af
Merge pull request #143 from zjs81/alpha6
chore: update version to alpha 6
2026-02-08 19:07:29 -08:00
446564
87a2807f5b chore: update version to alpha 6 2026-02-08 18:56:24 -08:00
Ryan Malloy
daca42701c
Notification rate limiting (#110)
* Add notification rate limiting with privacy-safe debug logging

- Add batching system to prevent notification storms (3s rate limit, 5s batch window)
- Queue rapid notifications and show batch summaries
- Debug logs show device names for adverts, sender/channel for messages (no content leaks)
- Remove unused _maxBatchSize constant

Context: Added after getting notification-flooded while evaluating RF flood management. The irony.

* Update notification_service.dart

I made a mistake and removed this

* Add l10n support for notification strings

Addresses PR #110 review feedback to use the translations system:
- Add notification strings to app_en.arb (plurals for batch summary)
- Update NotificationService to use lookupAppLocalizations()
- Wire locale from MaterialApp to NotificationService
- Regenerate localization files

New strings added (English only, translations needed):
- notification_activityTitle: "MeshCore Activity"
- notification_messagesCount: "{count} message(s)"
- notification_channelMessagesCount: "{count} channel message(s)"
- notification_newNodesCount: "{count} new node(s)"
- notification_newTypeDiscovered: "New {type} discovered"
- notification_receivedNewMessage: "Received new message"

* Add notification string translations for all supported languages

Translated notification_activityTitle, notification_messagesCount,
notification_channelMessagesCount, notification_newNodesCount,
notification_newTypeDiscovered, and notification_receivedNewMessage
to: bg, de, es, fr, it, nl, pl, pt, ru, sk, sl, sv, uk, zh

Includes proper ICU plural forms for Slavic languages (few/many/other)
and Slovenian dual form.

* Apply dart format to notification_service.dart

---------

Co-authored-by: Winston Lowe <wel97459@gmail.com>
2026-02-08 18:42:15 -08:00
Ded
ea43cf17eb
reduce map marker size (#131)
* reduce map marker size

reduces map markers from 80 to 60 px to improve visibility with higher density areas

* add flutter test to actions

* Add GPX export functionality and related UI components

* Refactor GPX export constants to use lowercase naming convention and improve export function error handling

* ran formating

* Enhance GPX export functionality with customizable parameters and improved metadata

* Implement PathTraceMapScreen and refactor path tracing functionality across screens

* Add localization for missing location error in path tracing

* Updated GPX export functionality for contacts and repeaters in multiple languages.

* Add scrollbar to path trace details list for improved navigation

* Integrate SharePlus plugin for enhanced sharing functionality across platforms

* reduce map marker size

reduces map markers from 80 to 60 px to improve visibility with higher density areas

* reduce marker size to improve map clarity and add path trace navigation to path management

---------

Co-authored-by: Winston Lowe <wel97459@gmail.com>
2026-02-08 18:40:58 -08:00
Ded
8ef6e2c656
Merge pull request #130 from zjs81/path-map-rotation
remove rotation in path map
2026-02-08 18:39:48 -08:00
Winston Lowe
24de98d5ee
Merge pull request #134 from zjs81/dev-gpx
Added a export to a GPX
2026-02-08 17:15:26 -08:00
Winston Lowe
0fd841b5b5
Merge branch 'main' into dev-gpx 2026-02-08 17:13:18 -08:00
Winston Lowe
c365b7889b
Merge pull request #141 from zjs81/dev-NewPathTracing
Implement PathTraceMapScreen and refactor path tracing functionality
2026-02-08 17:10:16 -08:00
Winston Lowe
2db30ace6a Integrate SharePlus plugin for enhanced sharing functionality across platforms 2026-02-08 12:26:49 -08:00
Winston Lowe
0d8801fa75 Add scrollbar to path trace details list for improved navigation 2026-02-08 12:25:51 -08:00
Winston Lowe
bcae6ac19f Updated GPX export functionality for contacts and repeaters in multiple languages. 2026-02-08 12:14:03 -08:00
Winston Lowe
2f4b230b31 Add localization for missing location error in path tracing 2026-02-08 11:57:04 -08:00
Winston Lowe
98e0b05e73 Implement PathTraceMapScreen and refactor path tracing functionality across screens 2026-02-08 11:32:36 -08:00
Winston Lowe
2a909e6081 Enhance GPX export functionality with customizable parameters and improved metadata 2026-02-07 19:45:02 -08:00
Winston Lowe
d1009d3c20 ran formating 2026-02-07 11:07:57 -08:00
Ded
91b1696bc5
Merge pull request #132 from zjs81/add-test-action
add flutter test to actions
2026-02-07 08:33:16 -08:00
Winston Lowe
978ea4790d Refactor GPX export constants to use lowercase naming convention and improve export function error handling 2026-02-05 13:46:05 -08:00
Winston Lowe
8b1228bf8d Add GPX export functionality and related UI components 2026-02-05 13:38:49 -08:00
446564
ddee76ced2 add flutter test to actions 2026-02-05 09:40:31 -08:00
446564
6a3c59fa2c remove rotation in path map
when zooming on the path map view window the rotation was too easy to trigger and
provided little value to understanding the path
2026-02-05 09:24:24 -08:00
Ded
a54cc78691
Merge pull request #129 from zjs81/remove-msg-prefix
remove direct msg notification prefix
2026-02-05 08:58:43 -08:00
446564
05fb5a13fa remove direct msg notification prefix
The prefix "New message from " takes up a lot of space and was not localized anyway.
2026-02-05 08:33:07 -08:00
zjs81
c320378be1
Refactor unread message tracking and implement channel caching (#126)
* Refactor unread message tracking and implement channel caching

* formatted files
2026-02-04 20:34:03 -07:00
Ded
b3645481c7
Merge pull request #125 from zjs81/reduce-build-steps
stop building twice for pull requests on branches from this repo
2026-02-04 12:42:24 -08:00
Ded
589707aa13
Merge pull request #123 from zjs81/dart-format
This formats the project and adds a workflow to check that each contribution has also been formatted.

plus small fix to get rid of analyzer errors which could be changed to warnings but that's another day.
2026-02-04 12:41:46 -08:00
446564
6070802213 stop building twice for pull requests
we should only run the build steps on a pull request OR a push to main
2026-02-04 09:02:03 -08:00
446564
2525b9425b reduce jobs for flutter and dart
no need to setup the env twice the exact same way as they don't conflict
2026-02-04 08:59:29 -08:00
446564
b786c90514 combine flutter and dart actions
reduce time to complete and stop running twice for pull requests
2026-02-04 08:56:40 -08:00
446564
a35590a407 fix dart format workflow install deps step
needs to use flutter pub get not dart pub get
2026-02-04 08:40:19 -08:00
446564
8d15f7cef6 wrap returns from if blocks
fixes two analyzer errors for return blocks on new lines from if blocks
2026-02-04 08:34:37 -08:00
446564
e449f5e1d5 add dart format workflow
checks code has been formatted with dart format on push and pull request

adds a note in README for contributors
2026-02-04 08:33:49 -08:00
446564
b34d684e67 format dart files
formats all dart files using `dart format .` from the root project dir

this makes the code style repeatable by new contributors and makes PR review easier
2026-02-04 08:32:35 -08:00
Ded
488a286701
Merge pull request #59 from 446564/community-#-names
add community to hashtag channel name
2026-02-03 20:08:42 -08:00
Zach
c742d98fbb issue #112 fixes and more 2026-02-01 18:37:14 -07:00
zjs81
1d4c9ad9bd
Merge pull request #115 from zjs81/advert-intervals
allow disable repeater adverts
2026-02-01 17:10:46 -07:00
Zach
818f514702 The first issue was that the toggle switch states weren't being initialized when settings were refreshed from the device. The code would correctly update the interval values themselves, but failed to set the corresponding boolean flags that control whether the toggles appear as "on" or "off". This meant that if you refreshed settings from a device that had advertisements disabled (with an interval of zero), the toggles would incorrectly show as enabled even though the device was actually broadcasting no advertisements. We fixed this by adding two lines that explicitly set _advertEnable = _advertInterval > 0 and _floodAdvertEnable = _floodAdvertInterval > 0 after parsing the interval values from device responses.
The second critical bug was in the validation logic that checks whether responses from the device contain valid data. The validator was rejecting any interval values of zero because it checked interval > 0, but zero is now a meaningful and valid value that indicates advertisements are disabled. Without this fix, any time a device reported back that advertisements were disabled, the app would silently discard that information as invalid, leaving the UI out of sync with reality. We changed the validation to use interval >= 0 instead and updated the comment to explicitly document that zero means disabled.

The third fix was a minor code style issue where a single-line if statement was missing braces, causing a linter warning. This doesn't affect functionality but ensures the code meets project standards.
2026-02-01 17:08:53 -07:00
Zach
be54419e5b Merge remote-tracking branch 'origin/main' into advert-intervals 2026-02-01 17:03:53 -07:00
zjs81
00eb1a68a6
Merge pull request #118 from wel97459/dev-shareContact
Adds contact shearing
2026-02-01 16:59:21 -07:00
Zach
79ffc21bd6 fix commit 2026-02-01 16:57:17 -07:00
Zach
0374f4f5da Merge remote-tracking branch 'origin/main' into dev-shareContact 2026-02-01 14:18:35 -07:00
Winston Lowe
4650584f9b
Merge pull request #117 from wel97459/dev-reconnection
This cures a race condition that was messing up the disconnection handler.
Before the bluetooth device was fully connected _handleDisconnection() was being called from the lisener.
2026-01-31 22:28:35 -08:00
Winston Lowe
8d8b938878 Ran translation script 2026-01-31 22:19:01 -08:00
Ded
e3a0bd3b13
Merge pull request #114 from zjs81/obtainum-btn
add obtainium badge
2026-01-31 20:07:08 -08:00
446564
4f83d87f8c use switch for advert enable/disable
move style to align with other toggles and use a switch instead of a checkbox
2026-01-31 17:07:24 -08:00
Winston Lowe
6d7d51f0a4 _requestDeviceInfo added isConnected not already _awaitingSelfInfo 2026-01-31 16:03:05 -08:00
Winston Lowe
33680f0cb9 Replace action buttons with a popup menu for better UI/UX on channels and map screens 2026-01-31 15:25:34 -08:00
Winston Lowe
5115d8bbe3 Added zero-hop contact sharing functionality and related UI updates 2026-01-31 15:00:33 -08:00
Winston Lowe
d30e7c4e2c Prevent disconnection handling when already disconnected, curing a race condition. 2026-01-31 14:55:55 -08:00
Winston Lowe
8470171e88 Merge branch 'dev-shareContacts' into dev-shareContact 2026-01-31 08:02:35 -08:00
446564
ede3142d40 allow disable repeater adverts
Adds checkbox to disable adverts and flood adverts

Also updates flood avert range to new max of 168 hours
2026-01-30 11:05:57 -08:00
446564
6712088fcd add obtainium badge
allow users to easily add app to obtainium
https://apps.obtainium.imranr.dev
2026-01-30 08:44:03 -08:00
Ded
7b519854d7
Merge branch 'main' into community-#-names 2026-01-29 08:07:05 -08:00
Zach
90ce46392a feat: optimize reaction message format to reduce airtime
- Reduce reaction payload from ~44 bytes to 9 bytes (5x smaller)
- Use 4-char hex hash (timestamp + sender + first 5 chars) for message ID
- Use 2-char hex emoji index instead of multi-byte UTF-8 emoji
- Format: r:HASH:INDEX (e.g., r:a1b2:00)
- For 1:1 chats, sender is implicit (null) for shorter hash
- Prevent users from reacting to their own messages
- Add room server reaction support with sender identification
- Make emoji lists public in EmojiPicker for shared indexing
- Add 💪 and 🚀 emojis to picker
- Add comprehensive unit tests for reaction helpers
- Update minor dependencies
2026-01-28 23:21:04 -07:00
Zach
d61ec217fc feat: add Russian and Ukrainian to language selector
These languages had translation files but were missing from the
settings UI. Adds appSettings_languageRu and appSettings_languageUk
strings and corresponding RadioListTile entries.

Fixes missing languages in app settings.
2026-01-28 22:26:14 -07:00
Zach
3ac81a5448 Merge origin/main into pr-106
Resolve conflict in app_de.arb: keep improved German translation
for community_updateSecret while adding path trace strings from main.
2026-01-28 22:22:43 -07:00
zjs81
7004067839
Merge pull request #108 from wel97459/dev-pathtrace
Path tracing. This adds support to ping and trace route repeaters and room server.
2026-01-28 22:07:14 -07:00
Zach
935b7b07eb Add path trace localizations for all languages
- Translate path trace strings to all 14 supported locales
- Regenerate localization Dart files
- Fix translate.py to also detect empty string values as missing
2026-01-28 22:05:04 -07:00
Zach
cdacc54421 Merge remote-tracking branch 'origin/main' into dev-pathtrace 2026-01-28 21:43:07 -07:00
zjs81
bf8f002d55
Merge pull request #111 from wel97459/dev-reconntion
Added disconnection handling, and fixed state changing of navigation on connection.
2026-01-28 21:39:42 -07:00
Zach
998ff50495 fix: restore _handleDisconnection() on battery request failure
This was the author's original intent - use battery request failure
as a signal that the connection is lost.
2026-01-28 21:34:13 -07:00
Zach
92d2b224e7 fix: address PR review issues
- Fix memory leak by adding dispose() to remove connection listener
- Fix typo: changedNavgation -> _changedNavigation
- Add mounted check before navigation to prevent errors
- Remove overly aggressive _handleDisconnection() call on battery request failure
- Only reset battery flag on error to allow retry without disconnecting
2026-01-28 21:29:18 -07:00
Winston Lowe
34a6b5d895 Added error catching to requestBatteryStatus
to call _handleDisconnection when it fails update.

Updated ScannerScreen to manage navigation state logic on connection.
2026-01-28 20:13:40 -08:00
zjs81
c953a1a798
Merge pull request #105 from erikklavora/main
Updated Slovenian lang
2026-01-28 20:53:49 -07:00
Winston Lowe
42115bf200 Refactor contact handling and enhance UI with new advert options and localized strings 2026-01-28 11:04:34 -08:00
Winston Lowe
d0c8fab6fb Add contact import functionality and update UI feedback for import status 2026-01-28 10:19:42 -08:00
Winston Lowe
eeb8ff34e8 Implement contact import functionality from clipboard and add relevant UI options 2026-01-26 16:11:21 -08:00
Winston Lowe
641307a316 Added response code for exporting contacts and implement frame listener in contacts_screen.dart 2026-01-26 12:19:45 -08:00
Winston Lowe
c37abb63e3 add export and import contact frame builders in meshcore_protocol.dart and implement contact export functionality in contacts_screen.dart 2026-01-26 11:56:42 -08:00
Winston Lowe
898ef1c11c Refactor autofocus logic in login dialogs for better platform handling 2026-01-26 10:40:10 -08:00
Winston Lowe
749f9d4dfd cleaned up. 2026-01-25 12:00:38 -08:00
Winston Lowe
9c1b5899fb Added scroll view to room server login.
Disabled autofocus of password.
2026-01-25 11:55:55 -08:00
Winston Lowe
cacb9bc677 Moved all the path tracing logic to the dialog.
refactored repeater hub along with contacts screen to use shortPubKeyHex.
Added localization strings for path tracing, english only.
2026-01-25 10:58:00 -08:00
Winston Lowe
0ebd688787 Added shortPubKeyHex
and added a trace route builder traceRouteBytes
2026-01-25 10:53:28 -08:00
ericz
bb18038f60 removed truncation of notification as in Issue #107 2026-01-25 11:40:02 +01:00
Winston Lowe
fcf741b20a Got the basic path tracing working. 2026-01-24 20:36:14 -08:00
ericz
88aa104ae5 further translation fixes for german 2026-01-24 18:05:10 +01:00
erikklavora
90f90ad7cf
Updated Slovenian lang 2026-01-24 17:05:01 +01:00
Zach
8b0bdd9a46 fix: update PRODUCT_BUNDLE_IDENTIFIER to com.monitormx.meshcoreopen 2026-01-24 01:37:19 -07:00
zjs81
45d914de57 chore: update version to 5.0.0+5 in pubspec.yaml 2026-01-24 01:26:23 -07:00
Zach
2c49534955 feat: add url_launcher_ios dependency and update project configuration 2026-01-24 01:24:56 -07:00
Zach
c56cf9c3ed feat: add CocoaPods support for macOS and iOS, including necessary configurations and dependencies 2026-01-24 01:07:18 -07:00
zjs81
fee4cd13be chore: update version to 0.4.5+4 in pubspec.yaml 2026-01-24 00:52:15 -07:00
zjs81
a53d5ccfb6
Merge pull request #69 from spfmoby/better-french-translations2
More french translation updates
2026-01-24 00:50:11 -07:00
zjs81
e5d06b1c7e
Merge pull request #102 from zjs81/pr-94
Pr 94
2026-01-24 00:46:48 -07:00
zjs81
e95a55e4f0 feat: add Ukrainian localization support and improve string formatting 2026-01-24 00:45:01 -07:00
zjs81
422ca941c2 Merge remote-tracking branch 'origin/main' into pr-94 2026-01-24 00:42:29 -07:00
zjs81
3098d860e9
Merge pull request #101 from zjs81/anupoh/main
Anupoh/main
2026-01-24 00:32:29 -07:00
zjs81
f0d34f7503 Update Russian localization for improved pluralization and add new chat link handling messages
- Enhanced pluralization rules for "hops" in various contexts to better reflect Russian grammar.
- Added new localization strings for chat link handling, including error messages and confirmation prompts.
- Ensured consistency in the use of plural forms across the application.
2026-01-24 00:27:45 -07:00
zjs81
daa0c3f9c3 Merge branch 'main' into anupoh/main 2026-01-24 00:22:28 -07:00
zjs81
09e1cd2b8d fix: improve BLE scanning reliability and filter out own node from contacts list improve text scaling 2026-01-24 00:17:18 -07:00
zjs81
fa514533eb feat: add ChatScrollController and JumpToBottomButton for improved chat scrolling experience
- Implemented ChatScrollController to manage scroll behavior and visibility of jump-to-bottom button.
- Added functionality to automatically scroll to the bottom when the keyboard opens.
- Created JumpToBottomButton widget that appears when the user scrolls up, allowing quick navigation back to the bottom of the chat.
2026-01-23 17:56:06 -07:00
zjs81
75b8b8af70
Merge pull request #60 from 446564/missing-tooltips
update tooltips
2026-01-23 16:47:31 -07:00
spfmoby
115667a27c More french translation updates6 2026-01-23 17:39:59 +01:00
spfmoby
cfb51d96ff More french translation updates6 2026-01-23 17:39:49 +01:00
anupoh
75356fe20d
Russian translation for the app
I've prepared the Russian localization files for the app. It would be great if localization were included in the app. Thanx a lot!
2026-01-23 16:58:16 +07:00
Winston Lowe
2089613696 Added the basics for path tracing 2026-01-22 23:42:10 -08:00
megadimich
c43df67fac Ukrainian localization files 2026-01-22 15:08:42 +00:00
spfmoby
e2b9b58d7d More french translation updates5 2026-01-22 10:25:42 +01:00
spfmoby
d6794bc8d7 More french translation updates4 2026-01-22 08:45:54 +01:00
spfmoby
72216e2cf7 More french translation updates3 2026-01-22 08:21:09 +01:00
spfmoby
2a2275ec31 More french translation updates2 2026-01-22 08:16:58 +01:00
spfmoby
dff037535d More french translation updates 2026-01-21 18:13:24 +01:00
zjs81
297e609b3e fix: replace RadioListTile with RadioGroup for better state management in community selection 2026-01-20 22:40:42 -07:00
zjs81
20171c491f fix: update iOS platform version and enable sentence capitalization in chat input fields 2026-01-20 22:28:37 -07:00
zjs81
cc43f4d198
Merge pull request #65 from zjs81/fix/message-length-safety-margin
fix: add safety margin to text message overhead calculations
2026-01-20 21:51:53 -07:00
zjs81
537384ea5b fix: add safety margin to text message overhead calculations 2026-01-20 21:50:35 -07:00
zjs81
a0be63b2e7 feat: integrate link handling in chat screen with linkify support
- Added flutter_linkify package to auto-detect and linkify URLs in chat messages.
- Implemented LinkHandler class to manage link tap confirmations and URL launching.
- Updated chat_screen.dart to use Linkify for displaying message text with links.
- Registered url_launcher plugin for handling URL launches across platforms.
- Updated pubspec.yaml and pubspec.lock to include new dependencies.
- Cleaned up untranslated.json by removing unused translations.
2026-01-20 21:42:54 -07:00
zjs81
1cc887e5bb
Merge pull request #61 from 446564/remove-rcvd
remove msg notify prefix when preview avail
2026-01-20 21:11:08 -07:00
446564
26d9029538 remove msg notify prefix when preview avail
this removes the 'Received new message: ' prefix from notications
when there is a message preview available
2026-01-20 17:35:14 -08:00
446564
30bcbedf5e update tooltips
add missing tooltip:
- channels, add channel button
- map, filter nodes button
2026-01-20 17:21:44 -08:00
446564
4003519deb add community to hashtag channel name
brings behavior in line with community public channels and prefixes the community name

this allows users to use the same radio with multiple clients and be able to tell which
hashtag channel they are using i.e. Scouts #leaders, where previous it was just a private
chanel named #leaders.
2026-01-20 15:26:41 -08:00
zjs81
3fdd8f5eaf chore: Update version to 0.4.0+4 in pubspec.yaml 2026-01-19 20:58:11 -07:00
zjs81
f4ec732de8 feat: Add community management features with QR code scanning
- Implement Community model for managing community data, including secret handling and PSK derivation.
- Create CommunityQrScannerScreen for scanning and joining communities via QR codes.
- Develop CommunityStore for persisting community data using SharedPreferences.
- Introduce QrCodeDisplay widget for displaying QR codes with customizable options.
- Add QrScannerWidget for reusable QR code scanning functionality with validation and controls.
2026-01-19 20:56:07 -07:00
zjs81
f790604d23
Merge pull request #42 from wel97459/dev-neighbours
Added Neighbors to the repeater hub and a screen to display the Neighbors
2026-01-19 19:17:00 -07:00
zjs81
8e3b563aba revert translate.py 2026-01-19 19:14:48 -07:00
zjs81
ee3b0a3126 Add untranslated messages file and update localization keys
- Added `untranslated.json` to track untranslated messages.
- Updated localization keys in various language files to use camelCase format for consistency.
- Modified `neighbours_screen.dart` to reference updated localization keys.
2026-01-19 19:13:22 -07:00
zjs81
31d633ee0b Merge main into dev-neighbours 2026-01-19 19:09:03 -07:00
zjs81
c269365d81
Merge pull request #48 from wel97459/dev-gps
Added GPS enable and GPS interval settings.
2026-01-19 19:02:13 -07:00
zjs81
9a9f59e53f localization: update GPS settings messages for clarity and consistency across multiple languages 2026-01-19 19:00:30 -07:00
zjs81
9cb667fad0 localization: fix punctuation in GPS interval settings for Spanish and Portuguese 2026-01-19 19:00:24 -07:00
zjs81
3fef594fe5 localization: update GPS settings messages and improve handling of custom variables 2026-01-19 18:56:06 -07:00
zjs81
8387304d2a Merge main into dev-gps
- Resolved localization conflicts by keeping both GPS settings and room management strings
- Merged room management features from main
- Merged map and contacts screen updates from main
2026-01-19 18:51:02 -07:00
zjs81
2acba9eb84
Merge pull request #51 from wel97459/dev-roomManagement
Added room server management
2026-01-19 18:34:51 -07:00
zjs81
30ba1799e1 localization: update room management strings in multiple languages and refactor room login handling 2026-01-19 18:29:53 -07:00
zjs81
13f9c5058a Merge branch 'main' into dev-roomManagement 2026-01-19 18:25:00 -07:00
Winston Lowe
98fc2d6e0a Updated gps setting to follow state of companion. 2026-01-19 16:57:46 -08:00
Winston Lowe
2becbb342c Added buildGetCustomVarsFrame
And added update to refreshDeviceInfo and _requestDeviceInfo.
Added parsing of Custom Vars
2026-01-19 16:55:39 -08:00
zjs81
5b2d5a494c
Merge pull request #47 from ericszimmermann/main
Disable Map rotation
2026-01-19 09:26:29 -07:00
Winston Lowe
153736d36e added roomserver management 2026-01-18 21:21:33 -08:00
Winston Lowe
6c8a149e1b fix a few translations and used _neighbourCount 2026-01-18 12:01:57 -08:00
Winston Lowe
b41ccee4f9 Merge branch 'main' into dev-neighbours 2026-01-18 11:27:19 -08:00
Winston Lowe
04a713bb76 Added a basic neighbours screen for repeaters 2026-01-18 11:17:47 -08:00
Winston Lowe
714aecd7e6 Added GPS enable and interval settings 2026-01-18 01:05:46 -08:00
Winston Lowe
2e1a5e0fbf added CMD_SET_CUSTOM_VAR to BLE debug 2026-01-18 01:03:45 -08:00
Winston Lowe
1f0b7d8d7b added buildSetCustomVarFrame and setCustomVar 2026-01-18 01:02:48 -08:00
ericszimmermann
dffea23ce2
Merge branch 'zjs81:main' into main 2026-01-17 20:47:56 +01:00
zjs81
e0a8fb7ec0
Merge pull request #44 from mtlynch/gh-build
Add a Github Action to build code in CI
2026-01-17 11:39:37 -07:00
zjs81
06fc08c41f
Merge pull request #45 from mtlynch/flutter-analyze
Fix issues flagged by flutter analyze
2026-01-17 11:38:08 -07:00
ericz
c22bfed680 Merge branch 'disable_map_rotation'
Disable Map Rotation.
2026-01-17 19:30:52 +01:00
zjs81
316c76e5b4
Merge pull request #46 from ericszimmermann/main
German translation V2
2026-01-17 11:20:54 -07:00
ericz
4b215ad574 Disable Map rotation 2026-01-17 17:14:39 +01:00
ericz
09e60cebd9 German translation V2 2026-01-17 17:03:39 +01:00
Michael Lynch
6782347cf4 Fix issues flagged by flutter analyze
This fixes code quality issues that flutter analyze catches and adds a CI step to Github Actions to flag on any future issues.
2026-01-17 11:00:34 -05:00
Michael Lynch
1726119c3e Add a Github Action to build code in CI
This adds a CI workflow in Github Actions to verify that the flutter builds compile for all supported platforms.

I tried adding Windows, but it currently fails, so I excluded it from this initial set.
2026-01-17 10:48:46 -05:00
zjs81
988806dccd
Merge pull request #41 from mtlynch/show-error
Show repeater login error in login dialog
2026-01-16 19:10:15 -07:00
zjs81
14ff8250c0 Add support for private and hashtag channels in localization and channel management
- Updated Polish, Portuguese, Slovak, Slovenian, Swedish, and Chinese localization files to include new strings for creating and joining private channels, as well as joining hashtag channels.
- Enhanced the channel management UI to allow users to create and join private channels, join public channels, and join channels via hashtags.
- Implemented PSK derivation from hashtags using SHA256 in the Channel model.
- Improved the translation script to handle missing keys and translate all locales efficiently.
2026-01-16 19:06:39 -07:00
Michael Lynch
2a04ebb8b6 Show repeater login error in login dialog 2026-01-16 09:35:02 -05:00
zjs81
a14462978d Replace Column with SingleChildScrollView in RepeaterLoginDialog for better layout handling 2026-01-15 21:49:54 -07:00
zjs81
df7fb45683
Merge pull request #38 from wel97459/dev-contactsPubkey
Added public key in contacts list and in the repeater hub
2026-01-15 19:26:53 -07:00
zjs81
f01eff07ff
Merge pull request #37 from wel97459/dev-map
Fix map centering
2026-01-15 19:20:20 -07:00
zjs81
7cc7183e0c Refactor map initialization and zoom calculation logic in MapScreen 2026-01-15 19:15:42 -07:00
zjs81
a6b2756d0d Ran flutter format on the file 2026-01-15 19:11:13 -07:00
zjs81
614f3d4601 Add signing configuration support in build.gradle.kts 2026-01-15 18:42:20 -07:00
zjs81
7c33647119 Add key.properties support for signing configuration in build.gradle.kts 2026-01-15 18:42:20 -07:00
zjs81
fde8b686f5
Merge pull request #28 from spfmoby/better-french-translations
Replace Publicité by Annonce in the french translations
2026-01-15 18:30:55 -07:00
zjs81
9bc3a27b53
Merge pull request #30 from dennis1248/main
Update Dutch translations
2026-01-15 18:30:02 -07:00
Winston Lowe
a8f387b0da Fix map centering weirdly
When nodes or markers are outside of the main area of interest.
2026-01-14 19:38:01 -08:00
Winston Lowe
dd1a73c247 Repeater hub now show public key at the top 2026-01-14 19:34:41 -08:00
Winston Lowe
e36f6b7eb9 changed contects list to show public keys of contect 2026-01-14 19:33:07 -08:00
Dennis ten Hoove
fcef82be63
Update Dutch translations
This solves many of the most obvious errors and inconsistensies in the initial translation.
2026-01-13 11:53:54 +01:00
spfmoby
6ddb8f1a3d more fr translations / .arb and .dart synced 2026-01-13 08:27:01 +01:00
spfmoby
7a22223756 Replace Publicité by Annonce in the french translations 2026-01-12 10:18:18 +01:00
zjs81
dba639abdc Bump version to 0.3.0+3 in pubspec.yaml 2026-01-11 19:06:54 -07:00
zjs81
1483fb7f1c Add battery polling functionality to MeshCoreConnector 2026-01-11 19:02:33 -07:00
zjs81
df04f315b4 Add Privacy Policy document outlining data collection practices and user rights 2026-01-11 18:12:31 -07:00
zjs81
c0f0c58518 Refactor radio settings to use nullable types and update command generation logic for improved safety 2026-01-11 18:08:44 -07:00
zjs81
01bd8243da Refactor timeout calculations for repeater and login frames to ensure minimum message size is respected; remove obsolete widget test file. 2026-01-11 17:40:19 -07:00
zjs81
b2ce82fe7e Add localization support and translation script
- Introduced a new extension for localization in Flutter with `LocalizationExtension` in `l10n.dart`.
- Added a Python script `translate.py` for translating ARB/JSON localization files using a local Ollama model, preserving keys and placeholders, and handling ICU format rules.
2026-01-11 17:13:50 -07:00
zjs81
2495cd840f
Merge pull request #16 from wel97459/dev-telemetry
Added telemetry to repeater management
2026-01-11 13:47:44 -07:00
zjs81
bc6c1f1fab Consolidate BufferReader/Writer, add response validation for repeater settings
- Move BufferReader/BufferWriter into meshcore_protocol.dart
- Refactor build functions to use BufferWriter
- Add content-based validation for CLI responses over LoRa
- Add individual refresh buttons for TX power and feature toggles
- Hide unimplemented features (Privacy Mode, Encrypted Advert Interval)
2026-01-11 13:44:01 -07:00
zjs81
310818f9d3
Merge pull request #27 from zjs81/dev-roomserver-fixes
Dev roomserver fixes
2026-01-11 11:52:45 -07:00
zjs81
8c3ffa5472 Refactor code for improved readability and null safety in various files Also updated PR to allow login via map. 2026-01-11 11:51:40 -07:00
zjs81
be3b920b3f Merge branch 'main' into dev-roomserver 2026-01-11 11:36:14 -07:00
zjs81
7703aaafc6
Merge pull request #26 from zjs81/dev-MapManageRepeater
Dev map manage repeater
2026-01-11 11:24:15 -07:00
zjs81
1ba3f3ac49 Merge branch 'main' into dev-MapManageRepeater 2026-01-11 11:21:21 -07:00
zjs81
ffbfd1a40c Refactor Manage Repeater button to close dialog before opening login 2026-01-11 11:17:23 -07:00
Winston Lowe
ab7cc84db5 moved roomserver chat into chat_screen 2026-01-09 23:44:42 -08:00
Winston Lowe
f3aef42331 changed noification to support messages from room server. 2026-01-09 00:04:30 -08:00
Winston Lowe
367f89fb1b Added value to Message fourByteRoomContactKey which holds the first 4 bytes of the contacts pub key that posted the message to the room. 2026-01-09 00:03:50 -08:00
zjs81
fe57963a26
Merge pull request #17 from wel97459/dev-icon-color
Fixed icons not being visible in Dark mode
2026-01-08 14:48:04 -07:00
Winston Lowe
fca810737d Working on Parsing room server messages. 2026-01-08 12:58:27 -08:00
Winston Lowe
35e866abfb Add login for room servers 2026-01-07 23:31:09 -08:00
Winston Lowe
ffce582b3b Change debug messages that I left and forgot 2026-01-07 10:45:30 -08:00
Winston Lowe
8c73359125 Fixed icons not being visible in Dark mode 2026-01-07 01:16:12 -08:00
Winston Lowe
401a3842ca Added loading message 2026-01-07 01:00:34 -08:00
Winston Lowe
2993ec1f49 Add to CayenneLpp parseByChannel function, and got basic ui working. 2026-01-07 00:53:56 -08:00
Winston Lowe
c306ad798c Added telemetry to repeater interface. 2026-01-07 00:50:20 -08:00
Winston Lowe
f5be9b9691 Added Manage Repeater to contact dialog from map view. 2026-01-05 16:41:46 -08:00
zach
e3d7607db9 fix overflowing widget and also add network perms for mac 2026-01-02 15:32:46 -07:00
zach
c44f0d1ae2 add notification perms 2026-01-02 14:58:13 -07:00
260 changed files with 151191 additions and 7133 deletions

78
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,78 @@
name: Build
on:
push:
branches:
- main
pull_request:
jobs:
android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties', 'android/build.gradle', 'android/settings.gradle', 'android/app/build.gradle', 'pubspec.lock') }}
restore-keys: |
${{ runner.os }}-gradle-
- run: flutter pub get
- run: flutter build apk --release --no-pub
ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- run: flutter pub get
- run: flutter build ios --release --no-codesign --no-pub
linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- name: Install Linux build deps
run: sudo apt-get update && sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev
- run: flutter pub get
- run: flutter build linux --release --no-pub
macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- run: flutter pub get
- run: flutter build macos --release --no-pub
web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- run: flutter pub get
- run: flutter build web --release --no-pub

38
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,38 @@
name: Deploy to Cloudflare Workers
on:
push:
tags:
- '*'
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
# Match local development version which provides Dart 3.11.0
flutter-version: '3.41.2'
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Get dependencies
run: flutter pub get
- name: Build Web
run: bun run build
- name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy

31
.github/workflows/flutter_dart.yml vendored Normal file
View file

@ -0,0 +1,31 @@
name: Flutter and Dart
on:
pull_request:
push:
branches:
- main
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
- name: Install dependencies
run: flutter pub get
- name: Analyze code
run: flutter analyze --fatal-infos --fatal-warnings
- name: Verify formatting
run: dart format --output=none --set-exit-if-changed .
- name: Run tests
run: flutter test -r github

10
.gitignore vendored
View file

@ -30,8 +30,12 @@ migrate_working_dir/
.flutter-plugins-dependencies .flutter-plugins-dependencies
.pub-cache/ .pub-cache/
.pub/ .pub/
pubspec.lock
/build/ /build/
/coverage/ /coverage/
# fvm project files
.fvm/
.fvmrc
# Symbolication related # Symbolication related
app.*.symbols app.*.symbols
@ -57,6 +61,7 @@ secrets.dart
.DS_Store .DS_Store
.AppleDouble .AppleDouble
.LSOverride .LSOverride
macos/Flutter/GeneratedPluginRegistrant.swift
# iOS # iOS
**/ios/Pods/ **/ios/Pods/
@ -65,11 +70,13 @@ secrets.dart
**/ios/Flutter/Flutter.podspec **/ios/Flutter/Flutter.podspec
# Android # Android
.gradle/
**/android/.gradle/ **/android/.gradle/
**/android/captures/ **/android/captures/
**/android/local.properties **/android/local.properties
**/android/.externalNativeBuild/ **/android/.externalNativeBuild/
*.jks *.jks
key.properties
keystore.properties keystore.properties
# Generated files # Generated files
@ -80,3 +87,6 @@ keystore.properties
# IDE # IDE
.vscode/launch.json .vscode/launch.json
.vscode/settings.json .vscode/settings.json
# Cloudflare Wrangler
.wrangler

0
.gitmodules vendored Normal file
View file

1
.ruby-version Normal file
View file

@ -0,0 +1 @@
4.0.0

1
.swift-version Normal file
View file

@ -0,0 +1 @@
6.2.4

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.

View file

@ -61,7 +61,7 @@ lib/
- **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device) - **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device)
### Device Discovery ### Device Discovery
- Scans for devices with name prefix `MeshCore-` - Scans for devices with known name prefixes
- Filters by `platformName` or `advertisementData.advName` - Filters by `platformName` or `advertisementData.advName`
### Connection States ### Connection States

71
CONTRIBUTING.md Normal file
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.

View file

@ -6,6 +6,10 @@ 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.
<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"/>
</a>
## Screenshots ## Screenshots
<table> <table>
@ -21,6 +25,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
## Features ## Features
### Core Functionality ### Core Functionality
- **Direct Messaging**: Private encrypted conversations with individual contacts - **Direct Messaging**: Private encrypted conversations with individual contacts
- **Public Channels**: Broadcast messages to channel subscribers on the mesh network - **Public Channels**: Broadcast messages to channel subscribers on the mesh network
- **Contact Management**: Organize contacts, track last seen times, and manage conversation history - **Contact Management**: Organize contacts, track last seen times, and manage conversation history
@ -29,6 +34,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
- **Message Replies**: Thread conversations with inline reply functionality - **Message Replies**: Thread conversations with inline reply functionality
### Mesh Network ### Mesh Network
- **Path Visualization**: View routing paths and signal quality for each contact - **Path Visualization**: View routing paths and signal quality for each contact
- **Route Management**: Manual path overriding and automatic route rotation - **Route Management**: Manual path overriding and automatic route rotation
- **Signal Metrics**: Real-time SNR (Signal-to-Noise Ratio) tracking - **Signal Metrics**: Real-time SNR (Signal-to-Noise Ratio) tracking
@ -36,6 +42,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
- **Repeater Support**: Connect to and manage repeater nodes for extended range - **Repeater Support**: Connect to and manage repeater nodes for extended range
### Map & Location ### Map & Location
- **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
@ -43,12 +50,14 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
- **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)
### Repeater Hub ### Repeater Hub
- **CLI Access**: Full command-line interface to repeater nodes - **CLI Access**: Full command-line interface to repeater nodes
- **Settings Management**: Configure repeater behavior, power limits, and network settings - **Settings Management**: Configure repeater behavior, power limits, and network settings
- **Statistics Dashboard**: View repeater traffic, connected clients, and system health - **Statistics Dashboard**: View repeater traffic, connected clients, and system health
@ -57,6 +66,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
## Technical Details ## Technical Details
### Architecture ### Architecture
- **Framework**: Flutter 3.38.5 / Dart 3.10.4 - **Framework**: Flutter 3.38.5 / Dart 3.10.4
- **State Management**: Provider pattern with ChangeNotifier - **State Management**: Provider pattern with ChangeNotifier
- **BLE Protocol**: Nordic UART Service (NUS) over Bluetooth Low Energy - **BLE Protocol**: Nordic UART Service (NUS) over Bluetooth Low Energy
@ -64,11 +74,20 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
- **Encryption**: End-to-end encryption for private messages using the MeshCore protocol - **Encryption**: End-to-end encryption for private messages using the MeshCore protocol
### Platform Support ### Platform Support
- ✅ **Android**: Full support (API 21+)
- ✅ **iOS**: Full support (iOS 12+) | Feature | Android (API 21+) | iOS (12+) | Linux | Windows | macOS | Web |
- 🚧 **Desktop**: Limited support (macOS/Linux/Windows) |--------------------|:-----------------:|:---------:|:-----:|:-------:|:-----:|:---------------------------------:|
| BLE companion | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| USB companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ✅ |
| TCP companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ❌<br>(requires websocket bridge) |
| Core Functionality | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Mesh Network | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Map & Location | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Device Management | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Repeater Hub | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
### Dependencies ### Dependencies
| Package | Purpose | | Package | Purpose |
|---------|---------| |---------|---------|
| flutter_blue_plus | Bluetooth Low Energy communication | | flutter_blue_plus | Bluetooth Low Energy communication |
@ -84,6 +103,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
## Getting Started ## Getting Started
### Prerequisites ### Prerequisites
- Flutter SDK 3.38.5 or later - Flutter SDK 3.38.5 or later
- Android Studio / Xcode (for mobile development) - Android Studio / Xcode (for mobile development)
- A MeshCore-compatible LoRa device - A MeshCore-compatible LoRa device
@ -91,17 +111,20 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
### Installation ### Installation
1. **Clone the repository** 1. **Clone the repository**
```bash ```bash
git clone https://github.com/zjs81/meshcore-open.git git clone https://github.com/zjs81/meshcore-open.git
cd meshcore-open cd meshcore-open
``` ```
2. **Install dependencies** 2. **Install dependencies**
```bash ```bash
flutter pub get flutter pub get
``` ```
3. **Run the app** 3. **Run the app**
```bash ```bash
flutter run flutter run
``` ```
@ -109,11 +132,13 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
### Building for Release ### Building for Release
**Android APK:** **Android APK:**
```bash ```bash
flutter build apk --release flutter build apk --release
``` ```
**iOS:** **iOS:**
```bash ```bash
flutter build ios --release flutter build ios --release
``` ```
@ -125,7 +150,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
@ -152,25 +178,39 @@ lib/
## BLE Protocol ## BLE Protocol
### Nordic UART Service (NUS) ### Nordic UART Service (NUS)
- **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e` - **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`
- **RX Characteristic**: `6e400002-b5a3-f393-e0a9-e50e24dcca9e` (Write to device) - **RX Characteristic**: `6e400002-b5a3-f393-e0a9-e50e24dcca9e` (Write to device)
- **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device) - **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device)
### 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_`
New device prefixes can be added in `lib/connector/meshcore_uuids.dart`.
### Message Format ### Message Format
Messages are transmitted as binary frames using a custom protocol optimized for LoRa transmission. See `meshcore_protocol.dart` for frame structure definitions. Messages are transmitted as binary frames using a custom protocol optimized for LoRa transmission. See `meshcore_protocol.dart` for frame structure definitions.
## Configuration ## Configuration
### 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
### Device Settings ### Device Settings
- **Radio Power**: Transmit power adjustment (10-30 dBm) - **Radio Power**: Transmit power adjustment (10-30 dBm)
- **Frequency**: LoRa frequency configuration - **Frequency**: LoRa frequency configuration
- **Bandwidth**: Channel bandwidth selection - **Bandwidth**: Channel bandwidth selection
@ -182,22 +222,24 @@ 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!
### Development Guidelines ### Development Guidelines
- Follow the Flutter style guide - Follow the Flutter style guide
- Use Material 3 design components - Use Material 3 design components
- Write clear commit messages - Write clear commit messages
- Test on both Android and iOS before submitting PRs - Test on both Android and iOS before submitting PRs
### Code Style ### Code Style
- Prefer `StatelessWidget` with `Consumer` for reactive UI - Prefer `StatelessWidget` with `Consumer` for reactive UI
- Use `const` constructors where possible - Use `const` constructors where possible
- Keep functions small and focused - Keep functions small and focused
- Avoid premature abstractions - Avoid premature abstractions
- Run dart format on all changes before submitting
## Support ## Support
For issues, questions, or feature requests, please open an issue on GitHub: For issues, questions, or feature requests, please open an issue on GitHub:
https://github.com/zjs81/meshcore-open/issues <https://github.com/zjs81/meshcore-open/issues>
## Donate ## Donate
@ -205,6 +247,11 @@ If you find MeshCore Open useful and would like to support development, you can
**Solana Address:** `F15YanjZj96YTBtKJYgNa8RLQLCZkx5CEwogPWkqXeoQ` **Solana Address:** `F15YanjZj96YTBtKJYgNa8RLQLCZkx5CEwogPWkqXeoQ`
**Monero Address:** `453TxnpUqjkJtXxzdjMsrgERNkBRXEGamPbpC45ENrvKAk9tH7kZbxWF82Hz66etgDZyXFPEBU2JUEqhLeJyWt9kBvTVy5m`
**Bitcoin Address:** `bc1qh45x28v8dslcg4v4upmqd9g0mvc3lnyffmyzr5`
Your support helps maintain and improve this open-source project! Your support helps maintain and improve this open-source project!
## Acknowledgments ## Acknowledgments

244
TESTFLIGHT_GUIDE.md Normal file
View file

@ -0,0 +1,244 @@
# TestFlight and App Store Deployment Guide
## Prerequisites
- [x] Apple Developer Account ($99/year) - [developer.apple.com](https://developer.apple.com)
- [x] Xcode installed
- [x] Apple Transporter app installed
- [x] App icons ready (1024x1024px)
- [x] Bundle ID configured: `com.monitormx.meshcoreopen`
## Step 1: Register Bundle Identifier
1. Go to [Apple Developer - Identifiers](https://developer.apple.com/account/resources/identifiers/list)
2. Click the **"+"** button
3. Select **"App IDs"** → Continue
4. Select **"App"** → Continue
5. Fill in:
- **Description**: Meshcore Open
- **Bundle ID**: Explicit - `com.monitormx.meshcoreopen`
- **Capabilities**: Leave defaults (or add as needed)
6. Click **Continue** → **Register**
## Step 2: Create App in App Store Connect
1. Go to [App Store Connect](https://appstoreconnect.apple.com)
2. Sign in with your Apple ID
3. Click **"My Apps"**
4. Click the **"+"** button → **"New App"**
5. Fill in the form:
- **Platforms**: iOS
- **Name**: Meshcore Open
- **Primary Language**: English (U.S.)
- **Bundle ID**: Select `com.monitormx.meshcoreopen` from dropdown
- **SKU**: `meshcore-open-001` (or any unique identifier)
- **User Access**: Full Access
6. Click **"Create"**
## Step 3: Build the IPA
Run these commands from the project directory:
```bash
# Add CocoaPods to PATH
export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH"
# Clean previous builds
../flutter/bin/flutter clean
# Build IPA for App Store
../flutter/bin/flutter build ipa
```
The IPA will be created at: `build/ios/ipa/meshcore_open.ipa`
## Step 4: Upload to App Store Connect via Transporter
1. **Open Apple Transporter**
- Launch from Applications folder
- Sign in with your Apple ID
2. **Upload the IPA**
- Drag and drop `build/ios/ipa/meshcore_open.ipa` into Transporter
- Click **"Deliver"**
- Wait for upload to complete (usually 1-5 minutes)
3. **Processing**
- Apple will process your build (10-30 minutes)
- You'll receive an email when processing is complete
## Step 5: Configure App Store Connect Metadata
### App Information
1. In App Store Connect, go to your app
2. Fill in required information:
- **Subtitle**: Short description (30 chars max)
- **Privacy Policy URL**: Required for Bluetooth apps
- **Category**: Utilities or Productivity
- **Age Rating**: Complete questionnaire
### App Store Listing
1. Go to **App Store** tab
2. Upload **Screenshots** (required):
- iPhone 6.7" display (1290 x 2796 pixels) - At least 1 screenshot
- iPhone 6.5" display (1242 x 2688 pixels) - At least 1 screenshot
- Optional: iPad screenshots
3. Fill in **Description**:
```
Meshcore Open is a Flutter client for MeshCore LoRa mesh networking devices.
Features:
- BLE connectivity to MeshCore devices
- Real-time mesh network communication
- Map visualization with OpenStreetMap
- Community management with QR code scanning
- Message tracking and retry system
Connect to your MeshCore LoRa device and start communicating over the mesh network.
```
4. **Keywords**: `lora,mesh,networking,bluetooth,communication`
5. **Support URL**: Your GitHub or website URL
6. **Marketing URL**: (Optional)
### Version Information
1. **What's New in This Version**:
```
Initial release of Meshcore Open
- BLE device connectivity
- Mesh network messaging
- Map integration
- Community features
```
2. **Build**: Select the uploaded build once processing completes
## Step 6: TestFlight Setup
### Internal Testing (No Review Required)
1. Go to **TestFlight** tab in App Store Connect
2. Click **Internal Testing****"+"** to create a group
3. Name your group (e.g., "Internal Testers")
4. Add yourself as a tester using your email
5. Select the build you uploaded
6. Testers will receive an email with TestFlight invitation
### External Testing (Requires Beta Review)
1. Click **External Testing****"+"** to create a group
2. Add build and testers
3. Fill in **Test Information**:
- **What to Test**: Brief description of features
- **Feedback Email**: Your email address
4. Click **Submit for Review**
5. Beta review typically takes 24-48 hours
## Step 7: App Store Submission
Once you're ready for public release:
1. Go to **App Store** tab
2. Complete all required metadata (if not done)
3. Select your build
4. Fill in **App Review Information**:
- **Contact Information**: Your name, phone, email
- **Demo Account**: If app requires login
- **Notes**: Any special instructions for reviewers
5. Answer **Export Compliance** questions:
- Does your app use encryption? **Yes** (uses TLS/HTTPS)
- Is encryption registration required? **No** (standard encryption)
6. Click **Add for Review**
7. Review summary and click **Submit to App Review**
## Step 8: After Submission
- **App Review**: Typically 24-48 hours
- **Common Rejection Reasons**:
- Missing privacy policy
- Incomplete app information
- Crashes or bugs
- Misleading app description
- **If Approved**: You can release immediately or schedule a release date
- **If Rejected**: Address issues and resubmit
## Updating the App
When you need to release an update:
1. **Update version** in `pubspec.yaml`:
```yaml
version: 0.5.0+6 # Increment version (0.5.0) and build number (+6)
```
2. **Build new IPA**:
```bash
export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH"
../flutter/bin/flutter clean
../flutter/bin/flutter build ipa
```
3. **Upload via Transporter** (same process as above)
4. **Create new version** in App Store Connect:
- Click **"+"** next to versions
- Select version number
- Update "What's New" text
- Select new build
- Submit for review
## macOS Build (Bonus)
To build for macOS:
```bash
export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH"
../flutter/bin/flutter build macos --release
cd build/macos/Build/Products/Release
zip -r meshcore_open-macos.zip meshcore_open.app
```
Distribution:
- Share the zip file directly
- Users unzip and drag to Applications
- First run: Right-click → Open (to bypass Gatekeeper)
## Troubleshooting
### Build Errors
- **CocoaPods not found**: Ensure PATH includes `/opt/homebrew/lib/ruby/gems/4.0.0/bin`
- **No signing certificate**: Configure Team in Xcode (Signing & Capabilities)
- **Bundle ID mismatch**: Check `ios/Runner.xcodeproj/project.pbxproj`
### Upload Errors
- **No profiles found**: Create app in App Store Connect first
- **Bundle ID not registered**: Register in Apple Developer portal
- **Authentication failed**: Use Transporter app instead of CLI
### TestFlight Issues
- **Build not appearing**: Wait 10-30 minutes for processing
- **Can't add testers**: Check you have available slots (100 internal, 10,000 external)
- **TestFlight crashes**: Check device logs in Xcode → Devices & Simulators
## Important Files
- **iOS IPA**: `build/ios/ipa/meshcore_open.ipa`
- **macOS App**: `build/macos/Build/Products/Release/meshcore_open.app`
- **Bundle ID Config**: `ios/Runner.xcodeproj/project.pbxproj`
- **Version Info**: `pubspec.yaml`
## Useful Links
- [App Store Connect](https://appstoreconnect.apple.com)
- [Apple Developer Portal](https://developer.apple.com/account)
- [TestFlight Documentation](https://developer.apple.com/testflight/)
- [App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/)
- [Flutter iOS Deployment](https://docs.flutter.dev/deployment/ios)
## Support
For issues with:
- **App Store Process**: [Apple Developer Support](https://developer.apple.com/contact/)
- **Flutter Build Issues**: [Flutter GitHub](https://github.com/flutter/flutter/issues)
- **Meshcore Open App**: [GitHub Issues](https://github.com/wel97459/meshcore-open/issues)

View file

@ -1,3 +1,5 @@
import java.util.Properties
plugins { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-android") id("kotlin-android")
@ -5,19 +7,25 @@ plugins {
id("dev.flutter.flutter-gradle-plugin") id("dev.flutter.flutter-gradle-plugin")
} }
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystorePropertiesFile.inputStream().use { keystoreProperties.load(it) }
}
android { 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_11 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString() jvmTarget = JavaVersion.VERSION_17.toString()
} }
defaultConfig { defaultConfig {
@ -40,11 +48,25 @@ android {
// } // }
} }
signingConfigs {
create("release") {
val storeFilePath = keystoreProperties["storeFile"] as String?
if (storeFilePath != null) {
storeFile = file(storeFilePath)
storePassword = keystoreProperties["storePassword"] as String?
keyAlias = keystoreProperties["keyAlias"] as String?
keyPassword = keystoreProperties["keyPassword"] as String?
}
}
}
buildTypes { buildTypes {
release { release {
// TODO: Add your own signing config for the release build. signingConfig = if (keystorePropertiesFile.exists()) {
// Signing with the debug keys for now, so `flutter run --release` works. signingConfigs.getByName("release")
signingConfig = signingConfigs.getByName("debug") } else {
signingConfigs.getByName("debug")
}
} }
} }
@ -61,5 +83,5 @@ flutter {
} }
dependencies { dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
} }

View file

@ -16,6 +16,10 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- Camera permission for QR code scanning -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
<application <application
android:label="meshcore_open" android:label="meshcore_open"
@ -64,5 +68,14 @@
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain"/>
</intent> </intent>
<!-- URL launcher intents for opening links -->
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="http"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https"/>
</intent>
</queries> </queries>
</manifest> </manifest>

View file

@ -1,5 +1,18 @@
package com.meshcore.meshcore_open package com.meshcore.meshcore_open
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity : FlutterActivity() class MainActivity : FlutterActivity() {
private val usbFunctions by lazy { MeshcoreUsbFunctions(this) }
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
usbFunctions.configureFlutterEngine(flutterEngine)
}
override fun onDestroy() {
usbFunctions.dispose()
super.onDestroy()
}
}

View file

@ -0,0 +1,582 @@
package com.meshcore.meshcore_open
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbConstants
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbEndpoint
import android.hardware.usb.UsbInterface
import android.hardware.usb.UsbManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class MeshcoreUsbFunctions(
private val activity: FlutterActivity,
) {
private companion object {
const val usbRecipientInterface = 0x01
}
private val usbMethodChannelName = "meshcore_open/android_usb_serial"
private val usbEventChannelName = "meshcore_open/android_usb_serial_events"
private val usbPermissionAction = "com.meshcore.meshcore_open.USB_PERMISSION"
private val usbManager by lazy {
activity.getSystemService(Context.USB_SERVICE) as UsbManager
}
private val mainHandler = Handler(Looper.getMainLooper())
private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor()
@Volatile private var eventSink: EventChannel.EventSink? = null
@Volatile private var usbConnection: UsbDeviceConnection? = null
@Volatile private var usbInEndpoint: UsbEndpoint? = null
@Volatile private var usbOutEndpoint: UsbEndpoint? = null
@Volatile private var controlInterface: UsbInterface? = null
@Volatile private var dataInterface: UsbInterface? = null
private var readThread: Thread? = null
@Volatile private var isReading = false
@Volatile private var connectedDeviceName: String? = null
private var pendingConnectResult: MethodChannel.Result? = null
private var pendingConnectPortName: String? = null
private var pendingConnectBaudRate: Int = 115200
private data class PortConfig(
val controlInterface: UsbInterface?,
val dataInterface: UsbInterface,
val inEndpoint: UsbEndpoint,
val outEndpoint: UsbEndpoint,
)
private val permissionReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
handleUsbDetached(intent)
return
}
usbPermissionAction -> Unit
else -> return
}
val result = pendingConnectResult
val portName = pendingConnectPortName
pendingConnectResult = null
pendingConnectPortName = null
if (result == null || portName == null) {
return
}
val device = findUsbDevice(portName)
if (device == null) {
result.error(
"usb_device_missing",
null,
null,
)
return
}
val granted =
intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
if (!granted || !usbManager.hasPermission(device)) {
result.error("usb_permission_denied", null, null)
return
}
openUsbDevice(device, pendingConnectBaudRate, result)
}
}
fun configureFlutterEngine(flutterEngine: FlutterEngine) {
registerUsbPermissionReceiver()
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, usbMethodChannelName)
.setMethodCallHandler { call, result ->
when (call.method) {
"listPorts" -> result.success(listUsbPorts())
"connect" -> handleUsbConnect(call, result)
"write" -> handleUsbWrite(call, result)
"disconnect" -> {
scheduleCloseUsbConnection {
result.success(null)
}
}
else -> result.notImplemented()
}
}
EventChannel(flutterEngine.dartExecutor.binaryMessenger, usbEventChannelName)
.setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
eventSink = events
}
override fun onCancel(arguments: Any?) {
eventSink = null
}
},
)
}
fun dispose() {
closeUsbConnection()
usbIoExecutor.shutdownNow()
try {
activity.unregisterReceiver(permissionReceiver)
} catch (_: IllegalArgumentException) {
}
}
private fun registerUsbPermissionReceiver() {
val filter =
IntentFilter().apply {
addAction(usbPermissionAction)
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
activity.registerReceiver(permissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
@Suppress("DEPRECATION")
activity.registerReceiver(permissionReceiver, filter)
}
}
private fun listUsbPorts(): List<String> {
return usbManager.deviceList.values.map { device ->
val productName = device.productName ?: "USB Serial Device"
val vendorProduct =
String.format(
Locale.US,
"VID:%04X PID:%04X",
device.vendorId,
device.productId,
)
"${device.deviceName} - $productName - $vendorProduct"
}
}
private fun handleUsbConnect(call: MethodCall, result: MethodChannel.Result) {
val portName = call.argument<String>("portName")
val baudRate = call.argument<Int>("baudRate") ?: 115200
if (portName.isNullOrBlank()) {
result.error("usb_invalid_port", null, null)
return
}
val device = findUsbDevice(portName)
if (device == null) {
result.error("usb_device_missing", null, null)
return
}
if (usbManager.hasPermission(device)) {
openUsbDevice(device, baudRate, result)
return
}
if (pendingConnectResult != null) {
result.error("usb_busy", null, null)
return
}
pendingConnectResult = result
pendingConnectPortName = portName
pendingConnectBaudRate = baudRate
val permissionIntent = PendingIntent.getBroadcast(
activity,
0,
Intent(usbPermissionAction).setPackage(activity.packageName),
pendingIntentFlags(),
)
usbManager.requestPermission(device, permissionIntent)
}
private fun handleUsbWrite(call: MethodCall, result: MethodChannel.Result) {
val data = call.argument<ByteArray>("data")
val connection = usbConnection
val endpoint = usbOutEndpoint
if (data == null) {
result.error("usb_invalid_data", null, null)
return
}
if (connection == null || endpoint == null) {
result.error("usb_not_connected", null, null)
return
}
usbIoExecutor.execute {
try {
writeToDevice(data)
mainHandler.post { result.success(null) }
} catch (error: Exception) {
mainHandler.post {
result.error("usb_write_failed", error.message, null)
}
}
}
}
private fun findUsbDevice(portName: String): UsbDevice? {
val devices = usbManager.deviceList.values
val exactMatch = devices.firstOrNull { it.deviceName == portName }
if (exactMatch != null) {
return exactMatch
}
val normalizedName = portName.substringBefore(" - ").trim()
return devices.firstOrNull { it.deviceName == normalizedName }
}
private fun openUsbDevice(
device: UsbDevice,
baudRate: Int,
result: MethodChannel.Result,
) {
usbIoExecutor.execute {
try {
closeUsbConnection()
val config = resolvePortConfig(device)
if (config == null) {
mainHandler.post {
result.error(
"usb_driver_missing",
null,
null,
)
}
return@execute
}
val connection = usbManager.openDevice(device)
if (connection == null) {
mainHandler.post {
result.error(
"usb_open_failed",
null,
null,
)
}
return@execute
}
if (!connection.claimInterface(config.dataInterface, true)) {
connection.close()
mainHandler.post {
result.error(
"usb_open_failed",
null,
null,
)
}
return@execute
}
if (config.controlInterface != null &&
config.controlInterface.id != config.dataInterface.id &&
!connection.claimInterface(config.controlInterface, true)
) {
connection.releaseInterface(config.dataInterface)
connection.close()
mainHandler.post {
result.error(
"usb_open_failed",
null,
null,
)
}
return@execute
}
usbConnection = connection
usbInEndpoint = config.inEndpoint
usbOutEndpoint = config.outEndpoint
controlInterface = config.controlInterface
dataInterface = config.dataInterface
configureDevice(connection, config, baudRate)
connectedDeviceName = device.deviceName
startReadLoop()
mainHandler.post {
result.success(null)
}
} catch (error: Exception) {
closeUsbConnection()
mainHandler.post {
result.error("usb_connect_failed", error.message, null)
}
}
}
}
private fun resolvePortConfig(device: UsbDevice): PortConfig? {
var preferredDataInterface: UsbInterface? = null
var preferredInEndpoint: UsbEndpoint? = null
var preferredOutEndpoint: UsbEndpoint? = null
var fallbackDataInterface: UsbInterface? = null
var fallbackInEndpoint: UsbEndpoint? = null
var fallbackOutEndpoint: UsbEndpoint? = null
var preferredControlInterface: UsbInterface? = null
for (interfaceIndex in 0 until device.interfaceCount) {
val usbInterface = device.getInterface(interfaceIndex)
var inEndpoint: UsbEndpoint? = null
var outEndpoint: UsbEndpoint? = null
for (endpointIndex in 0 until usbInterface.endpointCount) {
val endpoint = usbInterface.getEndpoint(endpointIndex)
if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) {
continue
}
when (endpoint.direction) {
UsbConstants.USB_DIR_IN -> if (inEndpoint == null) inEndpoint = endpoint
UsbConstants.USB_DIR_OUT -> if (outEndpoint == null) outEndpoint = endpoint
}
}
val hasDataPair = inEndpoint != null && outEndpoint != null
when {
usbInterface.interfaceClass == UsbConstants.USB_CLASS_COMM &&
preferredControlInterface == null -> {
preferredControlInterface = usbInterface
}
hasDataPair &&
usbInterface.interfaceClass == UsbConstants.USB_CLASS_CDC_DATA -> {
preferredDataInterface = usbInterface
preferredInEndpoint = inEndpoint
preferredOutEndpoint = outEndpoint
}
hasDataPair && fallbackDataInterface == null -> {
fallbackDataInterface = usbInterface
fallbackInEndpoint = inEndpoint
fallbackOutEndpoint = outEndpoint
}
}
}
val dataInterface = preferredDataInterface ?: fallbackDataInterface ?: return null
val inEndpoint = preferredInEndpoint ?: fallbackInEndpoint ?: return null
val outEndpoint = preferredOutEndpoint ?: fallbackOutEndpoint ?: return null
return PortConfig(preferredControlInterface, dataInterface, inEndpoint, outEndpoint)
}
private fun configureDevice(
connection: UsbDeviceConnection,
config: PortConfig,
baudRate: Int,
) {
val control = config.controlInterface ?: return
val lineCoding =
byteArrayOf(
(baudRate and 0xFF).toByte(),
((baudRate shr 8) and 0xFF).toByte(),
((baudRate shr 16) and 0xFF).toByte(),
((baudRate shr 24) and 0xFF).toByte(),
0, // stop bits: 1
0, // parity: none
8, // data bits
)
val lineCodingResult =
connection.controlTransfer(
UsbConstants.USB_DIR_OUT or
UsbConstants.USB_TYPE_CLASS or
usbRecipientInterface,
0x20,
0,
control.id,
lineCoding,
lineCoding.size,
1000,
)
if (lineCodingResult < 0) {
throw IllegalStateException("Failed to configure USB line coding")
}
val controlLineResult =
connection.controlTransfer(
UsbConstants.USB_DIR_OUT or
UsbConstants.USB_TYPE_CLASS or
usbRecipientInterface,
0x22,
0x0001, // DTR on, RTS off
control.id,
null,
0,
1000,
)
if (controlLineResult < 0) {
throw IllegalStateException("Failed to configure USB control line state")
}
}
private fun startReadLoop() {
val connection = usbConnection ?: return
val endpoint = usbInEndpoint ?: return
isReading = true
readThread =
Thread({
val packetSize = endpoint.maxPacketSize.coerceAtLeast(64)
val buffer = ByteArray(packetSize * 4)
try {
while (isReading) {
val bytesRead = connection.bulkTransfer(endpoint, buffer, buffer.size, 250)
if (!isReading) {
break
}
if (bytesRead <= 0) {
continue
}
val packet = buffer.copyOf(bytesRead)
mainHandler.post {
eventSink?.success(packet)
}
}
} catch (error: Exception) {
if (isReading) {
mainHandler.post {
eventSink?.error(
"usb_io_error",
error.message ?: "USB serial I/O error",
null,
)
}
scheduleCloseUsbConnection()
}
}
}, "MeshCoreUsbRead").also { thread ->
thread.isDaemon = true
thread.start()
}
}
private fun writeToDevice(data: ByteArray) {
val connection = usbConnection ?: throw IllegalStateException("USB connection missing")
val endpoint = usbOutEndpoint ?: throw IllegalStateException("USB output endpoint missing")
var offset = 0
val maxPacketSize = endpoint.maxPacketSize.coerceAtLeast(64)
while (offset < data.size) {
val chunkSize = minOf(maxPacketSize, data.size - offset)
val chunk = data.copyOfRange(offset, offset + chunkSize)
val bytesWritten = connection.bulkTransfer(endpoint, chunk, chunkSize, 1000)
if (bytesWritten != chunkSize) {
throw IllegalStateException("Short USB write: wrote $bytesWritten of $chunkSize bytes")
}
offset += chunkSize
}
}
private fun scheduleCloseUsbConnection(onComplete: (() -> Unit)? = null) {
usbIoExecutor.execute {
closeUsbConnection()
if (onComplete != null) {
mainHandler.post(onComplete)
}
}
}
@Synchronized
private fun closeUsbConnection() {
isReading = false
readThread?.interrupt()
if (readThread != null && readThread !== Thread.currentThread()) {
try {
readThread?.join(300)
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
}
}
readThread = null
val connection = usbConnection
val claimedControl = controlInterface
val claimedData = dataInterface
usbInEndpoint = null
usbOutEndpoint = null
controlInterface = null
dataInterface = null
usbConnection = null
if (connection != null) {
if (claimedControl != null) {
try {
connection.releaseInterface(claimedControl)
} catch (_: Exception) {
}
}
if (claimedData != null && claimedData.id != claimedControl?.id) {
try {
connection.releaseInterface(claimedData)
} catch (_: Exception) {
}
}
try {
connection.close()
} catch (_: Exception) {
}
}
connectedDeviceName = null
}
private fun handleUsbDetached(intent: Intent) {
val detachedDevice =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
}
val detachedName = detachedDevice?.deviceName ?: return
if (pendingConnectPortName == detachedName) {
pendingConnectResult?.error(
"usb_device_detached",
"USB device was removed before the connection completed",
null,
)
pendingConnectResult = null
pendingConnectPortName = null
}
if (connectedDeviceName == detachedName) {
scheduleCloseUsbConnection {
eventSink?.error(
"usb_device_detached",
"USB device was disconnected",
null,
)
}
}
}
private fun pendingIntentFlags(): Int {
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
flags = flags or PendingIntent.FLAG_MUTABLE
}
return flags
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M268-240 42-466l57-56 170 170 56 56-57 56Zm226 0L268-466l56-57 170 170 368-368 56 57-424 424Zm0-226-57-56 198-198 57 56-198 198Z"/></svg>

After

Width:  |  Height:  |  Size: 253 B

BIN
assets/images/mesh-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

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

104
docs/PRIVACY_POLICY.md Normal file
View file

@ -0,0 +1,104 @@
# Privacy Policy for MeshCore Open
**Last Updated:** January 11, 2026
## Introduction
MeshCore Open ("the App") is an open-source Flutter application for communicating with MeshCore LoRa mesh networking devices. This Privacy Policy explains how the App handles your information.
## Data Collection
### Data We Do NOT Collect
MeshCore Open does **not**:
- Collect personal information
- Send data to external servers (except map tile requests)
- Track your usage or behavior
- Use analytics services
- Require account creation
- Share any data with third parties
### Data Stored Locally on Your Device
The App stores the following data **locally on your device only**:
- **Messages**: Chat messages sent and received through the mesh network
- **Contacts**: Names and identifiers of mesh network contacts
- **App Settings**: Your preferences (theme, language, notification settings)
- **Channel Settings**: Configuration for mesh network channels
- **Message History**: Path history for message routing
- **Debug Logs**: Optional BLE and app debug logs (if enabled by user)
- **Cached Map Tiles**: Offline map data for the mapping feature
All locally stored data remains on your device and is never transmitted to us or any third party.
## Permissions
The App requires certain device permissions to function:
### Bluetooth Permissions
- **BLUETOOTH, BLUETOOTH_ADMIN** (Android 11 and below)
- **BLUETOOTH_SCAN, BLUETOOTH_CONNECT, BLUETOOTH_ADVERTISE** (Android 12+)
These permissions are used solely to discover and communicate with MeshCore hardware devices via Bluetooth Low Energy (BLE).
### Location Permission
- **ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION**
Required by Android for BLE scanning on Android 11 and below. The App does not track or store your location. Location data may be optionally shared over the mesh network if you choose to enable location sharing features.
### Internet Permission
- **INTERNET**
Used only for downloading map tiles from OpenStreetMap tile servers when using the map feature. No personal data is transmitted.
### Notification Permission
- **POST_NOTIFICATIONS** (Android 13+)
Used to display notifications for incoming messages when the app is in the background.
### Background Service Permissions
- **FOREGROUND_SERVICE, FOREGROUND_SERVICE_CONNECTED_DEVICE, WAKE_LOCK**
Used to maintain BLE connection with your MeshCore device while the app is in the background.
## Third-Party Services
### Map Tiles
The App uses OpenStreetMap tile servers to display maps. When viewing maps, your device's IP address may be visible to the tile server. No other data is shared. See [OpenStreetMap's Privacy Policy](https://wiki.osmfoundation.org/wiki/Privacy_Policy) for more information.
### GIF Search (Giphy)
The App includes a GIF picker feature powered by Giphy. When you use the GIF search feature:
- Your search queries are sent to Giphy's API servers
- Your device's IP address is visible to Giphy
- Giphy may collect usage data according to their privacy policy
GIF search is optional and only activated when you choose to use it. See [Giphy's Privacy Policy](https://support.giphy.com/hc/en-us/articles/360032872931-GIPHY-Privacy-Policy) for more information about how they handle data.
## Mesh Network Communications
Messages sent through the MeshCore mesh network are transmitted over radio frequencies to other mesh devices. The App itself does not control or monitor these communications beyond facilitating the connection between your mobile device and your MeshCore hardware.
## Data Security
All data is stored locally on your device using standard Flutter/Android storage mechanisms. The App does not implement additional encryption for locally stored data beyond what the operating system provides.
## Children's Privacy
The App does not knowingly collect any personal information from children under 13 years of age.
## Open Source
MeshCore Open is open-source software. You can review the complete source code to verify these privacy practices at [the project repository].
## Changes to This Policy
We may update this Privacy Policy from time to time. Any changes will be reflected in the "Last Updated" date at the top of this policy.
## Contact
If you have questions about this Privacy Policy or the App's privacy practices, please open an issue on the project's GitHub repository.
---
**Summary**: MeshCore Open is a privacy-respecting app that stores all data locally on your device. We do not collect, track, or share your personal information.

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

30
documentation/README.md Normal file
View file

@ -0,0 +1,30 @@
# MeshCore Open - Feature Documentation
MeshCore Open is an open-source Flutter client for MeshCore LoRa mesh networking devices. This documentation covers every user-facing feature, how to access it, and what it does.
## Table of Contents
1. [Scanner & Connection](scanner-and-connection.md) - BLE scanning, USB serial, and TCP connection
2. [Navigation](navigation.md) - App flow, device screen, and quick-switch navigation
3. [Contacts](contacts.md) - Contact management, groups, discovery, and sharing
4. [Chat & Messaging](chat-and-messaging.md) - Direct messages, message status, reactions, and retries
5. [Channels](channels.md) - Broadcast channels, communities, and channel chat
6. [Map & Location](map-and-location.md) - Node map, path tracing, line-of-sight, and offline caching
7. [Settings](settings.md) - Device settings, app settings, radio configuration, and exports
8. [Notifications](notifications.md) - System notifications, unread badges, and notification preferences
9. [Repeater Management](repeater-management.md) - Repeater hub, status, CLI, telemetry, and neighbors
10. [Additional Features](additional-features.md) - GIF picker, localization, debug logs, SMAZ compression, and more
11. [BLE Protocol & Data Layer](ble-protocol.md) - Technical reference for the communication protocol and data architecture
## App Overview
MeshCore Open connects to MeshCore LoRa mesh radios over BLE, USB, or TCP. Once connected, users can:
- **Chat** with other mesh nodes via encrypted direct messages
- **Broadcast** on shared channels (public, hashtag, private, or community-scoped)
- **View nodes on a map** with GPS locations, predicted positions, and path traces
- **Manage repeaters** with CLI access, telemetry, neighbor info, and settings
- **Share contacts** via `meshcore://` URIs and QR codes
- **Configure radio settings** including frequency, power, bandwidth, and spreading factor
- **Cache offline maps** for use without internet connectivity
- **Analyze line-of-sight** between nodes with terrain elevation profiles

View file

@ -0,0 +1,187 @@
# 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 (15)
English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian
### How It Works
- All UI strings go through Flutter's ARB localization system
- Language can follow the system locale or be explicitly overridden
- Changes take effect immediately
---
## Discovered Contacts Screen
### How to Access
From Contacts screen → overflow menu → "Discovered Contacts"
### What the User Sees
A list of nodes heard passively over the air but not yet added as contacts. Each shows:
- Color-coded avatar (by type)
- Name
- Short public key
- Last-seen time
### Key Interactions
- Search bar with debounced filtering
- Sort by last seen or name; filter by type
- **Tap**: Import the contact (adds to your contact list)
- **Long-press**: Add Contact, Copy `meshcore://` URI to clipboard, or Delete
- Overflow menu → "Delete All" (with confirmation)
- Already-known contacts and your own node are filtered out
---
## SMAZ Compression
### What It Is
An optional per-contact and per-channel text compression feature using the SMAZ algorithm (optimized for short English text).
### How to Enable
- **Per contact**: Chat screen → info button → toggle "SMAZ compression"
- **Per channel**: Long-press channel → Edit → toggle "SMAZ compression"
### How It Works
- When enabled, compression is applied using a "compress only if smaller" strategy — the message is only transmitted compressed if the encoded result is actually shorter than the original. Otherwise, the original text is sent uncompressed
- Compressed messages are transmitted with a `s:` prefix followed by base64-encoded data
- Recipients using MeshCore Open will decompress automatically. **Recipients using other software** that is not SMAZ-aware will see garbled `s:...` text
- The codec operates on ASCII. Non-ASCII / non-English text generally does not benefit from compression and may even expand. Best suited for short English messages
- Disabled by default
---
## Community QR Scanner
### How to Access
From Channels screen → "+" FAB → "Scan Community QR"
### What the User Sees
A live QR scanner view with instruction text overlay.
### Key Interactions
- Scan a community QR code shared by another member
- On valid scan: confirmation dialog showing community name and ID
- Option to "Add public channel to device" on join
- If already a member: shows an "Already a member" dialog
- Invalid QR: shows an orange error snackbar
---
## Channel Message Path Viewing
### How to Access
In a channel chat, tap a message bubble (mobile) or use the "Path" action (desktop).
### What the User Sees
- Summary card: sender, time, repeat count, path type, observed hops
- "Other Observed Paths" section (if multiple paths detected)
- "Repeater Hops" section listing each hop with hex prefix, resolved name, and GPS coordinates
### Actions
- **Radar icon**: Opens path trace map for live trace
- **Map icon**: Opens a map with hop markers and polyline
- **Path dropdown**: Switch between observed path variants (if multiple)
---
## Debug Logging
### BLE Debug Log
**Access**: Settings → BLE Debug Log
Two views:
- **Frames**: Each BLE frame with direction, description, hex preview, timestamp. Long-press to copy hex.
- **Raw Log RX**: Decoded LoRa packets with route type, payload type, path bytes, and summary.
### App Debug Log
**Access**: Settings → App Debug Log (must be enabled first in App Settings → Debug)
Structured log entries with level (Info/Warning/Error), tag, message, and timestamp.
Both logs support copy-all and clear operations.
---
## Chrome Required Screen
### When It Appears
Automatically shown on web platforms when a non-Chromium browser is detected.
### What the User Sees
A full-screen informational page explaining that Web Bluetooth requires a Chromium-based browser. No interactive elements — purely informational.
---
## Path History Service
### What It Does (Background Service)
Maintains an in-memory LRU cache of up to 50 contacts, each with up to 100 route history entries, tracking:
- Hop count and trip time
- Success/failure counts and route weights
- Flood vs. direct discovery
### Path Scoring
Paths are scored using a weighted formula: reliability (45%), route weight (20%), latency (25%), and freshness (10%). These weights are internal and not user-configurable. Paths whose weight drops to zero or below are automatically deleted. Flood deliveries that receive an ACK give a weight boost (+0.5) to the specific return path.
Used internally for:
- **Auto route rotation**: Cycles through known paths using configurable weights on retries, with a diversity window to avoid re-using recently tried paths
- **Path selection**: Picks the best-scored path for each retry attempt
- **Flood statistics**: Tracks flood vs. direct discovery ratios
---
## Message Retry Service
### What It Does (Background Service)
Handles reliable delivery of outgoing direct messages:
1. Assigns a UUID and sends immediately. Only one message per contact can be in-flight at a time (avoids overflowing the firmware's 8-entry ACK table); subsequent messages are queued
2. Listens for ACK frames matched via SHA-256 hash of `[timestamp][attempt][text][sender_pubkey]`
3. On timeout, retries with exponential backoff: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s...)
4. Each retry may use a different path (via path history diversity window)
5. After max retries: marks failed but keeps a **30-second grace window** during which a late ACK can still resolve the message to "delivered". Optionally clears the contact's path
6. Reports RTT and path data for quality learning
7. Maintains an ACK hash history (last 50 entries) to handle duplicate ACKs
### Configurable Settings (App Settings → Messaging)
- Max retries (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
- Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost)

View file

@ -0,0 +1,254 @@
# BLE Protocol & Data Layer
This is a technical reference for the communication protocol and data architecture.
## Transport Layer
The app supports three transports, all sharing the same command/response protocol:
| Transport | Method | Implementation |
|---|---|---|
| Bluetooth LE | Nordic UART Service (NUS) GATT | `flutter_blue_plus` |
| USB Serial | Packet-framed serial | `MeshCoreUsbManager` |
| TCP | Packet-framed socket | `MeshCoreTcpConnector` |
### BLE (Nordic UART Service)
- **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`
- **RX Characteristic** (write to device): `6e400002-b5a3-f393-e0a9-e50e24dcca9e`
- **TX Characteristic** (notify from device): `6e400003-b5a3-f393-e0a9-e50e24dcca9e`
Raw `Uint8List` payloads are written directly to the RX characteristic. Writes use "write without response" if supported, falling back to "write with response".
### USB and TCP Framing
Both use a lightweight packet framing codec:
```
TX (host → device): [0x3C][len_lo][len_hi][payload...]
RX (device → host): [0x3E][len_lo][len_hi][payload...]
```
- Frame start: `0x3C` (`<`) for outgoing, `0x3E` (`>`) for incoming
- Length: 2-byte little-endian, payload only
- Max payload: 172 bytes
- TCP: `tcpNoDelay: true` (Nagle disabled), writes serialized to prevent interleaving
- USB: 10ms post-write delay between frames
## Connection State Machine
```
enum MeshCoreConnectionState {
disconnected,
scanning,
connecting,
connected,
disconnecting,
}
```
## BLE Connection Lifecycle
1. **Scan** with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`):
- `MeshCore-`
- `Whisper-`
- `WisCore-`
- `HT-`
- `LowMesh_MC_`
2. **Connect** with 15-second timeout
3. **Request MTU** 185 bytes (non-web only)
4. **Discover services** and locate NUS
5. **Enable TX notifications** (up to 3 attempts on native)
6. **Subscribe** to TX characteristic for incoming frames
7. **Initial sync**: device info query, time sync, channel sync
## Auto-Reconnect (BLE Only)
On unexpected disconnection, auto-reconnect with exponential backoff:
- Delays: 1s, 2s, 4s, 8s, 16s, 30s, 30s...
- Resets on successful connection
- Disabled for manual disconnects
- Not available for USB or TCP
## Protocol Constants
| Constant | Value | Description |
|---|---|---|
| Max frame size | 172 bytes | BLE/USB/TCP payload limit |
| Public key size | 32 bytes | Ed25519 public key |
| Max path size | 64 bytes | Maximum path data |
| Max name size | 32 bytes | Maximum node name |
| Max text payload | 160 bytes | Firmware `MAX_TEXT_LEN` |
| App protocol version | 3 | Sent in device query |
| Contact frame size | 148 bytes | Fixed-size contact record |
## Command Codes (App → Device)
| Code | Name | Description |
|------|------|-------------|
| 1 | CMD_APP_START | Announce app connection |
| 2 | CMD_SEND_TXT_MSG | Send direct text message |
| 3 | CMD_SEND_CHANNEL_TXT_MSG | Send channel text message |
| 4 | CMD_GET_CONTACTS | Request contact list |
| 5 | CMD_GET_DEVICE_TIME | Query device clock |
| 6 | CMD_SET_DEVICE_TIME | Set device clock |
| 7 | CMD_SEND_SELF_ADVERT | Broadcast own advertisement |
| 8 | CMD_SET_ADVERT_NAME | Set node name |
| 9 | CMD_ADD_UPDATE_CONTACT | Add or update a contact |
| 10 | CMD_SYNC_NEXT_MESSAGE | Request next queued message |
| 11 | CMD_SET_RADIO_PARAMS | Set radio parameters |
| 12 | CMD_SET_RADIO_TX_POWER | Set TX power |
| 13 | CMD_RESET_PATH | Reset contact path |
| 14 | CMD_SET_ADVERT_LATLON | Set advertised location |
| 15 | CMD_REMOVE_CONTACT | Remove a contact |
| 16 | CMD_SHARE_CONTACT | Share contact to mesh |
| 17 | CMD_EXPORT_CONTACT | Export contact as bytes |
| 18 | CMD_IMPORT_CONTACT | Import contact from bytes |
| 19 | CMD_REBOOT | Reboot device |
| 20 | CMD_GET_BATT_AND_STORAGE | Query battery and storage |
| 22 | CMD_DEVICE_QUERY | Query device info |
| 26 | CMD_SEND_LOGIN | Login to repeater/room |
| 27 | CMD_SEND_STATUS_REQ | Request repeater status |
| 30 | CMD_GET_CONTACT_BY_KEY | Get contact by public key |
| 31 | CMD_GET_CHANNEL | Get channel definition |
| 32 | CMD_SET_CHANNEL | Set channel name and PSK |
| 36 | CMD_SEND_TRACE_PATH | Request path trace |
| 38 | CMD_SET_OTHER_PARAMS | Set misc parameters |
| 39 | CMD_GET_TELEMETRY_REQ | Request sensor telemetry |
| 40 | CMD_GET_CUSTOM_VAR | Get custom variables |
| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable |
| 50 | CMD_SEND_BINARY_REQ | Send binary request |
| 57 | CMD_SEND_ANON_REQ | Send anonymous request |
| 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration |
| 59 | CMD_GET_AUTO_ADD_CONFIG | Get auto-add configuration |
## Response / Push Codes (Device → App)
| Code | Name | Description |
|------|------|-------------|
| 0 | RESP_CODE_OK | Generic success |
| 1 | RESP_CODE_ERR | Generic error |
| 2 | RESP_CODE_CONTACTS_START | Contact list begins |
| 3 | RESP_CODE_CONTACT | Single contact data |
| 4 | RESP_CODE_END_OF_CONTACTS | Contact list complete |
| 5 | RESP_CODE_SELF_INFO | Device self-info response |
| 6 | RESP_CODE_SENT | Message transmitted; carries `[1]=is_flood, [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 |
| 25 | RESP_CODE_AUTO_ADD_CONFIG | Auto-add flags |
| 0x80 | PUSH_CODE_ADVERT | Known contact re-seen |
| 0x81 | PUSH_CODE_PATH_UPDATED | Better path found; carries the 32-byte public key of the updated contact |
| 0x82 | PUSH_CODE_SEND_CONFIRMED | Delivery ACK from remote; carries ACK hash (4 bytes) + trip time (4 bytes) |
| 0x83 | PUSH_CODE_MSG_WAITING | Offline messages queued |
| 0x85 | PUSH_CODE_LOGIN_SUCCESS | Repeater/room login succeeded |
| 0x86 | PUSH_CODE_LOGIN_FAIL | Repeater/room login failed |
| 0x87 | PUSH_CODE_STATUS_RESPONSE | Repeater status response |
| 0x88 | PUSH_CODE_LOG_RX_DATA | Radio RX data with SNR (int8, units 1/4 dB), RSSI, and raw radio packet |
| 0x89 | PUSH_CODE_TRACE_DATA | Path trace result |
| 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered |
| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data |
| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response |
## Data Models
### Contact
32-byte public key (primary identity), name, type (chat/repeater/room/sensor), flags, path data, GPS coordinates, last-seen timestamp. Parsed from 148-byte firmware frames with this layout:
```
[0] = resp_code
[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

164
documentation/channels.md Normal file
View file

@ -0,0 +1,164 @@
# Channels
## Overview
Channels are broadcast group-chat spaces secured by a 16-byte pre-shared key (PSK). Any device with the same channel index and PSK will receive and decrypt channel messages. Unlike direct messages, channel messages are broadcast to the entire mesh.
Up to 8 channels (indices 07) can be active simultaneously on one device.
## How to Access
QuickSwitchBar tab 1 (middle) from any main screen.
## Channel Types
| Type | Icon | Color | Description |
|---|---|---|---|
| Public | Globe | Green | Fixed well-known PSK; any device can join |
| Hashtag | Hash tag | Blue | PSK derived from the hashtag name via SHA-256; discoverable by convention |
| Private | Lock | Blue | Random PSK; requires out-of-band sharing of the 32-hex key |
| Community | Groups/Tag | Purple | PSK derived via HMAC-SHA256 from a community's shared secret |
## Channels List Screen
### What the User Sees
- **Search bar** with live text filtering (300ms debounce)
- **Sort/filter button**
- **Scrollable list of channel cards**, each showing:
- Type icon with color coding (purple badge overlay for community channels)
- Channel name (or "Channel N" if unnamed)
- Subtitle: "Public channel", "Hashtag channel", "Private channel", or "Community channel - {name}"
- Unread badge (if messages are unread)
- Drag handle (when manual sort is active)
- **"+" FAB** to add a new channel
- **Overflow menu**: Disconnect, Manage Communities (only shown when at least one community exists), Settings
If no channels exist, an empty state with an "Add Public Channel" shortcut is shown. If a search produces no results, a separate "no results" empty state with a search-off icon is shown.
Pull-to-refresh (swipe down) forces a re-fetch of channels from the device firmware.
### Sorting Options
- **Manual** (default): Drag-and-drop reordering, persisted (drag handles are hidden when a search query is active)
- **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), or SMAZ compression toggle (compresses outgoing messages to allow longer text within the byte limit) |
| Mute / Unmute | Toggle push notification suppression for this channel |
| Delete | Remove the channel from the device (confirmation required) |
## Channel Chat
Tap a channel card to open the channel chat screen.
### App Bar
- Type icon (public/private/hashtag)
- Channel name
- Subtitle: "{type} - {N} unread"
### Message Display
- Reverse-scrolling list (newest at bottom)
- **Incoming messages**: Colored avatar with sender's initial (or first emoji if name starts with one; color is deterministic from sender name hash), sender name in primary color, message bubble
- **Outgoing messages**: Primary container color bubble with a small status icon: pending (clock), sent (checkmark), or failed (red error circle)
- Automatic older-message loading on scroll-to-top
- Jump-to-bottom button when scrolled up
- **Pinch-to-zoom**: Two-finger zoom (0.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
- **Mobile**: Tap a message bubble to view its routing path
- **Desktop**: Long-press/right-click → "Path" (tapping the bubble does nothing on desktop)
- Opens the Channel Message Path Screen (see [Additional Features](additional-features.md))
### Context Actions (Long-Press / Right-Click)
| Action | Availability | Description |
|---|---|---|
| Reply | All messages | Triggers reply mode |
| Path | Desktop only | Opens message path view |
| Add Reaction | Incoming messages only | Opens emoji picker (cannot react to your own messages) |
| Copy | All messages | Copies text to clipboard |
| Delete | All messages | Removes locally (not from mesh) |
### Message Path Viewing
Tap a message bubble to open the Channel Message Path Screen, which shows:
- Each hop in the path as a visual chain
- Known contacts identified by name at each hop
- Observed vs. declared hop counts
- Alternative path variants (if received via multiple paths)
- Map view buttons for geographic path visualization
## Communities
Communities are a layer above channels that provide a private namespace.
### What is a Community?
A community has a name and a 32-byte random secret. Channel PSKs are derived from this secret:
- **Public channel**: `HMAC-SHA256(secret, "channel:v1:__public__")[:16]`
- **Hashtag channel**: `HMAC-SHA256(secret, "channel:v1:{hashtag}")[:16]`
Outsiders who don't know the secret cannot discover or join community channels.
### Sharing a Community
Communities are shared via QR codes containing a JSON payload:
```json
{"v": 1, "type": "meshcore_community", "name": "...", "k": "<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
- **Delete** — removes the community locally and deletes all associated device channels (confirmation dialog warns how many channels will be removed)
## How Channels Differ from Direct Messages
| Aspect | Channels | Direct Messages |
|---|---|---|
| Addressing | Broadcast to all nodes with matching PSK | Point-to-point to a specific contact |
| Encryption | Shared PSK (symmetric) | Contact's public key (asymmetric) |
| Sender identity | Plain text prefix in payload | Verified via public key |
| Replies | Supported (swipe or long-press) | Not supported |
| Retry mechanism | No automatic retry | Exponential backoff with path rotation |

View file

@ -0,0 +1,120 @@
# Chat & Messaging
## Overview
The app supports two chat modes:
- **Direct messages**: Encrypted point-to-point messages to individual contacts
- **Channel messages**: Broadcast messages to shared channels (see [Channels](channels.md))
This page covers direct messaging. For channel chat, see the Channels documentation.
## How to Access
From the Contacts screen, tap any Chat-type contact to open the ChatScreen.
## Chat Screen Layout
### App Bar
- **Title**: Contact name
- **Subtitle**: Current routing path label (e.g., "2 hops", "flood (auto)", "direct (forced)") and unread count. Tapping the subtitle shows the full path details.
- **Action buttons**:
- **Routing mode** (waves icon): Switch between Auto, Direct, and Flood routing
- **Path management** (timeline icon): View recent paths with hop count, round-trip time, age, and success count. Paths are color-coded by direct repeater (green/yellow/red/blue for ranked repeaters, grey for unknown). Tap a path to activate it (the device verifies and confirms via snackbar), long-press to view full path details, set custom paths, or force flood mode. A warning banner appears when history reaches 100 entries.
- **Info** (info icon): Contact info dialog showing type, path, GPS coordinates, public key, and SMAZ compression toggle
### Message List
- Scrollable list with newest messages at the bottom
- **Outgoing messages**: Right-aligned, primary color background. **Failed messages** change to a red-toned error container background
- **Incoming messages**: Left-aligned, grey background with a colored avatar (initial letter or first emoji of sender name; color is deterministic from a hash of the sender name)
- Bubble width capped at 65% of screen width
- Hyperlinks rendered as tappable green underlined text
- **Pinch-to-zoom**: Two-finger zoom (0.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
- **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")
- Status icon
- 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 `3000 + 3000 × path_length` milliseconds (15000ms for flood)
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 | Mobile: tap bubble directly; Desktop: long-press/right-click menu | Shows message routing path |
| Copy | All messages | Copies text to clipboard |
| 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
documentation/contacts.md Normal file
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
- Automatically shown after connecting to a device
- QuickSwitchBar tab 0 (leftmost) from Channels or Map screens
- Back navigation from Chat or Settings screens
## Contact Types
| Type | Avatar Color | Icon | Description |
|---|---|---|---|
| Chat | Blue | Chat bubble | Another user's mesh radio |
| Repeater | Orange | Cell tower | A mesh repeater/relay node |
| Room | Purple | Group | A room server for group chat |
| Sensor | Green | 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 |
|---|---|---|
| Path Trace / Ping | Repeaters, Rooms (always); Chat if `pathLength > 0` | Opens PathTraceMapScreen. Label shows "Ping" when no path bytes are known, "Path Trace" otherwise |
| Manage Repeater | Repeaters only | Login dialog → RepeaterHubScreen |
| Room Login | Rooms only | Login dialog → ChatScreen |
| Room Management | Rooms only | Login dialog → RepeaterHubScreen (management mode) |
| Open Chat | Chat/Sensor | Same as single tap |
| Add/Remove Favorite | All types | Toggles the favorite flag |
| Share Contact | All types | 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 **two separate popup menus** in the app bar:
**Antenna icon menu** (contact sharing):
- 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
- Add Contact from Clipboard — reads a `meshcore://<hex>` URI from clipboard and imports it
**Three-dot overflow menu**:
- Disconnect — disconnects from the device
- Discovered Contacts — opens the DiscoveryScreen
- Settings — opens the Settings screen
## 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
Antenna menu → "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 (Add, Copy URI, Delete). The Discovery screen has its own search bar, type filters (Users, Repeaters, Rooms, Favorites), and sort options (Last Seen, A-Z). An overflow "Delete All" option clears all discovered contacts.
## 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.

View file

@ -0,0 +1,186 @@
# 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 12 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 (Semi-Transparent)
Many contacts on the mesh don't have GPS hardware, so the map has no explicit coordinates for them. Instead of leaving these contacts invisible, the app **infers an approximate position** by analyzing the repeater path the contact's messages travel through. These inferred positions are displayed as semi-transparent markers with a `not_listed_location` icon, visually distinct from confirmed-location markers.
#### 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 the average (centroid) of all anchor coordinates, with a smaller offset radius (80120m) applied for visual separation.
6. **Assign confidence level**:
- **High confidence** (2+ anchors): Displayed at 55% opacity.
- **Low confidence** (1 anchor): Displayed at 30% opacity.
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
- **Semi-transparent marker** with a `not_listed_location` icon: This is a guessed position, not a confirmed GPS fix.
- **More opaque** (55%): Higher confidence — the contact was seen through 2 or more repeaters with known positions.
- **More transparent** (30%): 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.
Additional filters:
- **Key prefix filter**: Show only contacts whose public key starts with a given prefix
- **Last-seen time slider**: From 1 hour to "all time"
### 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
- **Purple semi-transparent endpoint**: Target with guessed position
A legend card at the bottom lists each hop pair with SNR quality icons and total path distance.
### 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 collapsible control panel containing:
- **Elevation profile chart**: Terrain fill (green), LOS beam line (white), radio horizon line (yellow)
- **Status**: Clear (green) or blocked (red) with distance and minimum clearance
- **Options panel**: Node toggles, endpoint dropdowns, antenna height sliders (0400 ft), Run LOS button
### Key Interactions
- **Long-press the map** to add custom endpoints (orange 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) or red (blocked)
- Terrain elevation is fetched from the Open-Meteo API (2181 sample points, 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** slider (318) 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.

View file

@ -0,0 +1,87 @@
# Navigation
## App Flow
The app follows this general flow:
```
Launch → Scanner Screen → [Connect via BLE/USB/TCP] → Contacts 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.
## Device Screen
The Device Screen is a transitional hub that shows after connection. In practice, the app navigates directly to Contacts after connecting, but the Device Screen is reachable via the QuickSwitchBar.
### What the User Sees
**App Bar**:
- Left: Battery indicator chip (tappable — toggles between percentage and voltage display). Icon changes based on level: `battery_unknown` when data unavailable, `battery_alert` (orange) at 15% or below, `battery_full` otherwise
- Left-aligned title (`centerTitle: false`): Two-line layout — small grey "MeshCore" label above the device name in bold
- Right: Disconnect button (`bluetooth_disabled` crossed-out icon) and Settings button (tune icon)
**Body**:
- **Connection Card**: Device avatar, device name, device ID, "Connected" chip, and battery chip
- **Quick Switch** section: The QuickSwitchBar widget for navigating to Contacts/Channels/Map
### Disconnection
- The disconnect button shows a confirmation dialog before disconnecting
- If the device disconnects unexpectedly, the app automatically navigates back to the Scanner screen (fires after the current frame completes via a post-frame callback)
- 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 15 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 → ContactsScreen
├─ [TCP FAB] → push → TcpScreen
│ └─ [TCP connected] → pushReplacement → ContactsScreen
└─ [USB FAB] → push → UsbScreen
└─ [USB connected] → pushReplacement → ContactsScreen
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 button] → push → PathTraceMapScreen
├─ [terrain button] → push → LineOfSightMapScreen
└─ [long-press] → share marker / set location
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.

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 node 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 "99+" 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`): "MeshCore Activity — 2 messages, 1 channel message, 3 new nodes". Note: batch summaries are Android-only; on Apple platforms individual notifications are shown
## 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 |

View file

@ -0,0 +1,186 @@
# 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 stops immediately after the first attempt — only timeouts trigger retries
- After 5 failed attempts, further login attempts are blocked
---
## 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" based on contact type:
| Card | Destination |
|---|---|
| Status | Repeater Status Screen |
| Telemetry | Telemetry Screen |
| CLI | Repeater CLI Screen |
| Neighbors | Neighbors Screen |
| Settings | Repeater Settings Screen |
---
## Repeater Status
### What the User Sees
Three information cards:
**System Information**:
- Battery percentage
- Uptime
- Queue length
- Error flags
- Clock at login time
**Radio Statistics**:
- Last RSSI and SNR
- Noise floor
- TX and RX airtime
**Packet Statistics**:
- Packets sent, received, and duplicates
- Broken down by flood vs. direct
### Key Interactions
- Auto-queries the repeater on open; shows a loading spinner until data arrives
- On timeout: red snackbar error. On success: data appears with a green snackbar confirmation
- Pull-to-refresh or refresh button to re-query
- Routing mode popup and path management dialog in app bar (these controls appear on **all** management sub-screens, not just Status)
---
## 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 common commands (get name, get radio, get tx, neighbors, ver, advert, clock)
- **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
- Bug report icon: 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
**General**: `advert`, `reboot`, `clock`, `password`, `ver`, `clear stats`
**Settings**: `set name`, `set af`, `set tx`, `set repeat`, `set allow.read.only`, `set flood.max`, `set int.thresh`, `set agc.reset.interval`, `set multi.acks`, `set advert.interval`, `set flood.advert.interval`, `set guest.password`, `set lat`, `set lon`, `set radio`, `set rxdelay`, `set txdelay`, `set direct.txdelay`, `set bridge.*`, `set adc.multiplier`, `tempradio`, `setperm`
**Bridge**: `get bridge.type`
**Logging**: `log start`, `log stop`, `log erase`
**Neighbors**: `neighbors`, `neighbor.remove`
**Region Management**: `region`, `region load/get/put/remove/allowf/denyf/home/save`
**GPS**: `gps`, `gps on/off/sync/setloc/advert`
---
## 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
Five configuration cards:
**1. Basic Settings**
- Name field
- Admin password field
- Guest password field
**2. Radio Settings**
- Frequency (MHz)
- TX Power (dBm)
- Bandwidth dropdown (kHz)
- Spreading Factor (SF5SF12)
- Coding Rate (4/54/8)
**3. Location Settings**
- Latitude and longitude fields
**4. Features**
- Packet forwarding toggle
- Guest access toggle
**5. Advertisement Settings**
- Local advert interval slider (60240 minutes) with enable/disable toggle
- Flood advert interval slider (3168 hours) with enable/disable toggle
**6. Danger Zone** (red-styled card)
- Reboot repeater
- Erase filesystem (serial-only warning)
### Key Interactions
- **Settings are NOT auto-fetched on open**. Only name and location are pre-filled from locally cached contact data. You must tap each section's refresh button to fetch live values from the repeater
- TX Power has its own separate refresh button, independent from the main Radio Settings refresh
- Save button appears when changes are detected
- Settings are sent sequentially with 200ms delays between commands (fire-and-forget, no per-command acknowledgment wait)
- Validation prevents invalid values (e.g., frequency range, LoRa parameter compatibility)
- Advertisement interval sliders reset to defaults when re-enabled (local: 60 min, flood: 3 hours)
- **Erase Filesystem** does NOT send any command over the air — tapping it only shows a snackbar explaining the operation requires physical serial access. It is effectively non-functional when connected wirelessly

View file

@ -0,0 +1,124 @@
# 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.
**Bottom FAB Row**: Up to three floating action buttons:
- **USB** button - Opens USB connection screen (Android, Windows, Linux, macOS, Chrome web only)
- **TCP/IP** button - Opens TCP connection screen (all non-web platforms)
- **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 with names starting with `MeshCore-` or `Whisper-`
- 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
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 Contacts screen
7. On failure, shows a red error snackbar
---
## USB Connection
### How to Access
From the Scanner screen, tap the **USB** FAB button.
### 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)
- "Connect" button
- FABs at the bottom 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 or its Connect button to connect
- On successful connection, navigates to Contacts 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** FAB button.
### What the User Sees
- A colored status bar at the top
- **Host address** text field
- **Port number** text field
- **Connect** button
- FABs at the bottom 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 Contacts 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

169
documentation/settings.md Normal file
View file

@ -0,0 +1,169 @@
# 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. [App Settings](#app-settings) (link to sub-screen)
3. [Node Settings](#node-settings)
4. [Actions](#actions)
5. [Debug](#debug)
6. [Export](#export)
7. [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.
---
## App Settings
A dedicated sub-screen for app-level preferences (nothing here is sent to the device). All settings persist locally via SharedPreferences.
### Appearance
- **Theme**: System / Light / Dark
- **Language**: System default or one of 15 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian)
- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages
### Notifications
- **Master enable/disable**: Requests OS permission when enabling
- **Message notifications**: New direct message alerts
- **Channel message notifications**: New channel message alerts
- **Advertisement notifications**: New node discovery alerts
### Messaging
- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail
- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off):
- Max Route Weight (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)
### Battery
- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage)
### Map Display
- **Show Repeaters**: Toggle repeater markers on map
- **Show Chat Nodes**: Toggle chat node markers
- **Show Other Nodes**: Toggle room/sensor markers
- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week
- **Units**: Metric / Imperial
- **Offline Map Cache**: Navigate to tile download screen
### Debug
- **App Debug Logging**: Enable the in-app debug log
---
## Node Settings
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**: 19 regional presets — selecting a preset immediately fills all fields below. Full list: Australia, Australia (Narrow), Australia SA/WA/QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, Switzerland, USA Arizona, USA/Canada, Vietnam, Off-Grid 433, Off-Grid 869, Off-Grid 918
- **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
### 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 Mode
Opens a confirmation dialog with three buttons: Cancel, Enable, and Disable. Both states can be set from the same dialog regardless of current state. A snackbar confirms which state was applied. When on, the node stops broadcasting its location in advertisements.
---
## Actions
One-tap device operations:
| Action | Description |
|---|---|
| Send Advertisement | Floods the mesh with your node's advertisement |
| 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 orange) |
---
## 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
---
## Export
Three GPX export options (not available on web):
| Option | Exports |
|---|---|
| Export Repeaters | Repeaters and Rooms with GPS coordinates |
| Export Contacts | Chat contacts with GPS coordinates |
| Export All | All contacts with GPS coordinates |
Each creates a `.gpx` file and opens the OS share sheet. Feedback via snackbar for four outcomes: success, no contacts with coordinates, feature not available (web), or error.
---
## About
Shows the standard Flutter about dialog with app name, version, and legal notice.

61
flake.lock generated Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1770562336,
"narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d6c71932130818840fc8fe9509cf50be8c64634f",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

86
flake.nix Normal file
View file

@ -0,0 +1,86 @@
{
description = "MeshCore Flutter Application";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
# Flutter and Dart
flutter
dart
# Java (required for Android development)
jdk17
# Android development tools
android-tools
gradle
# For the shell hook to set up the environment for Flutter development
gtk3
glib
sysprof
libclang
cmake
ninja
pkg-config
libdatrie
# Additional tools for installing Android SDK if not present
curl
unzip
];
shellHook = ''
echo "MeshCore Flutter Development Environment"
export PKG_CONFIG_PATH="${pkgs.gtk3}/lib/pkgconfig:${pkgs.glib}/lib/pkgconfig:${pkgs.sysprof}/lib/pkgconfig:$PKG_CONFIG_PATH"
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [pkgs.gtk3 pkgs.glib pkgs.sysprof pkgs.libdatrie]}:$LD_LIBRARY_PATH"
export CMAKE_INSTALL_PREFIX="$PWD/build/bundle"
# Setup Android SDK in home directory (standard location)
export ANDROID_HOME="$HOME/Android/Sdk"
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools/bin:$PATH"
echo "Android SDK: $ANDROID_HOME"
echo ""
# Check if Android SDK exists and offer to download if not
if [ ! -d "$ANDROID_HOME" ]; then
echo "WARNING: Android SDK not found at $ANDROID_HOME"
echo ""
echo "To download and set up the Android SDK, run this command:"
echo ""
cat << 'EOF'
mkdir -p ~/Android/Sdk && cd ~/Android/Sdk && \
curl -o cmdline-tools.zip ${if pkgs.stdenv.isDarwin then "https://dl.google.com/android/repository/commandlinetools-mac-10406996_latest.zip" else "https://dl.google.com/android/repository/commandlinetools-linux-10406996_latest.zip"} && \
unzip -q cmdline-tools.zip && \
mkdir -p cmdline-tools/latest && \
mv cmdline-tools/* cmdline-tools/latest/ 2>/dev/null || echo "Warning: failed to move Android cmdline-tools into 'latest' directory; please check your SDK layout." >&2 && \
rm cmdline-tools.zip && \
cd cmdline-tools/latest/bin && \
yes | ./sdkmanager --sdk_root=~/Android/Sdk 'platform-tools' && \
echo "Android SDK setup complete!"
EOF
echo ""
echo "Then run 'flutter doctor' again to verify."
echo ""
else
echo "Android SDK found at $ANDROID_HOME"
fi
echo "To check that everything is set up correctly, run 'flutter doctor' and ensure there are no issues."
'';
};
}
);
}

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>

View file

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"

View file

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"

View file

@ -1,4 +1,4 @@
platform :ios, '12.0' 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

74
ios/Podfile.lock Normal file
View file

@ -0,0 +1,74 @@
PODS:
- Flutter (1.0.0)
- flutter_blue_plus_darwin (0.0.2):
- Flutter
- FlutterMacOS
- flutter_foreground_task (0.0.1):
- Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- package_info_plus (0.4.5):
- Flutter
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- 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_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- share_plus (from `.symlinks/plugins/share_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`)
EXTERNAL SOURCES:
Flutter:
:path: Flutter
flutter_blue_plus_darwin:
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
flutter_foreground_task:
:path: ".symlinks/plugins/flutter_foreground_task/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/darwin"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
share_plus:
:path: ".symlinks/plugins/share_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"
SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
PODFILE CHECKSUM: e42b502c78c33aa1ed9d42eaea8960ce2139504b
COCOAPODS: 1.16.2

View file

@ -14,6 +14,7 @@
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 */; };
9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4268181FCF3E12817B700E9C /* libPods-Runner.a */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -42,9 +43,13 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
24A76623340E493BD4C25C5C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
40AC50CE3E1D4278E82498CF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
4268181FCF3E12817B700E9C /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
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>"; };
@ -62,6 +67,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -94,6 +100,8 @@
97C146F01CF9000F007C117D /* Runner */, 97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */, 331C8082294A63A400263BE5 /* RunnerTests */,
DEE6F094D3B70E76087722E1 /* Pods */,
DAE613E34DF694C2E33B64C7 /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -121,6 +129,25 @@
path = Runner; path = Runner;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
DAE613E34DF694C2E33B64C7 /* Frameworks */ = {
isa = PBXGroup;
children = (
4268181FCF3E12817B700E9C /* libPods-Runner.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
DEE6F094D3B70E76087722E1 /* Pods */ = {
isa = PBXGroup;
children = (
40AC50CE3E1D4278E82498CF /* Pods-Runner.debug.xcconfig */,
24A76623340E493BD4C25C5C /* Pods-Runner.release.xcconfig */,
718BC7DCCFC5C370705C12E5 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -145,12 +172,15 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */, 97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */,
B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@ -253,6 +283,61 @@
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;
};
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";
};
DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
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;
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@ -346,7 +431,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;
@ -368,7 +453,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen; PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -384,7 +469,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -401,7 +486,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -416,7 +501,7 @@
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@ -472,7 +557,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;
@ -523,7 +608,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;
@ -547,7 +632,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen; PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@ -569,7 +654,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen; PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View file

@ -4,4 +4,7 @@
<FileRef <FileRef
location = "group:Runner.xcodeproj"> location = "group:Runner.xcodeproj">
</FileRef> </FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace> </Workspace>

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)
}
} }

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,46 @@
<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>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,17 +81,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>
</dict> </dict>
</plist> </plist>

6
l10n.yaml Normal file
View file

@ -0,0 +1,6 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false
untranslated-messages-file: untranslated.json

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,70 @@
import 'dart:async';
import 'dart:typed_data';
import '../services/app_debug_log_service.dart';
import '../services/tcp_transport_service.dart';
/// Manages TCP transport for MeshCore devices.
///
/// Owns the [TcpTransportService] and TCP-specific connection state.
/// The main [MeshCoreConnector] delegates all TCP operations here.
class MeshCoreTcpConnector {
final TcpTransportService _service = TcpTransportService();
AppDebugLogService? _debugLog;
StreamSubscription<Uint8List>? _frameSubscription;
// --- Getters ---
String? get activeEndpoint => _service.activeEndpoint;
bool get isConnected => _service.isConnected;
// --- Configuration ---
void setDebugLogService(AppDebugLogService? service) {
_debugLog = service;
_service.setDebugLogService(service);
}
// --- Connection lifecycle ---
Future<void> connect({required String host, required int port}) async {
_debugLog?.info('TcpConnector.connect endpoint=$host:$port', tag: 'TCP');
await _frameSubscription?.cancel();
_frameSubscription = null;
await _service.connect(host: host, port: port);
_debugLog?.info(
'TcpConnector.connect done, endpoint=${_service.activeEndpoint}',
tag: 'TCP',
);
}
StreamSubscription<Uint8List> listenFrames({
required void Function(Uint8List) onFrame,
required void Function(Object, StackTrace?) onError,
required void Function() onDone,
}) {
_frameSubscription = _service.frameStream.listen(
onFrame,
onError: onError,
onDone: onDone,
);
return _frameSubscription!;
}
Future<void> cancelFrameSubscription() async {
await _frameSubscription?.cancel();
_frameSubscription = null;
}
Future<void> disconnect() async {
if (!_service.isConnected && _frameSubscription == null) return;
_debugLog?.info('TcpConnector.disconnect', tag: 'TCP');
await _frameSubscription?.cancel();
_frameSubscription = null;
await _service.disconnect();
}
Future<void> write(Uint8List data) => _service.write(data);
void dispose() {
_frameSubscription?.cancel();
_service.dispose();
}
}

View file

@ -0,0 +1,78 @@
import 'dart:typed_data';
import '../services/app_debug_log_service.dart';
import '../services/usb_serial_service.dart';
/// Manages USB serial transport for MeshCore devices.
///
/// Owns the [UsbSerialService] and USB-specific connection state.
/// The main [MeshCoreConnector] delegates all USB operations here.
class MeshCoreUsbManager {
MeshCoreUsbManager();
final UsbSerialService _service = UsbSerialService();
AppDebugLogService? _debugLog;
String? _activePortKey;
String? _activePortLabel;
// --- Getters ---
String? get activePortKey => _activePortKey;
String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey;
bool get isConnected => _service.isConnected;
Stream<Uint8List> get frameStream => _service.frameStream;
// --- Configuration ---
Future<List<String>> listPorts() => _service.listPorts();
void setRequestPortLabel(String label) => _service.setRequestPortLabel(label);
void setFallbackDeviceName(String label) =>
_service.setFallbackDeviceName(label);
void setDebugLogService(AppDebugLogService? service) {
_debugLog = service;
_service.setDebugLogService(service);
}
// --- Connection lifecycle ---
Future<void> connect({
required String portName,
int baudRate = 115200,
}) async {
_debugLog?.info(
'UsbManager.connect: portName=$portName baud=$baudRate',
tag: 'USB',
);
await _service.connect(portName: portName, baudRate: baudRate);
_activePortKey = _service.activePortKey ?? portName;
_activePortLabel = _service.activePortDisplayLabel ?? portName;
_debugLog?.info(
'UsbManager.connect: done, key=$_activePortKey label=$_activePortLabel',
tag: 'USB',
);
}
Future<void> disconnect() async {
if (!_service.isConnected && _activePortKey == null) {
return;
}
_debugLog?.info('UsbManager.disconnect', tag: 'USB');
await _service.disconnect();
_activePortKey = null;
_activePortLabel = null;
}
Future<void> write(Uint8List data) => _service.write(data);
Future<void> writeRaw(Uint8List data) => _service.writeRaw(data);
// --- Label management ---
void updateConnectedLabel(String selfName) {
_service.updateConnectedLabel(selfName);
_activePortLabel = _service.activePortDisplayLabel ?? _activePortLabel;
}
void dispose() {
_service.dispose();
}
}

View file

@ -1,6 +1,178 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/widgets.dart';
// Buffer Reader - sequential binary data reader with pointer tracking
class BufferReader {
int _pointer = 0;
int _lastPointer = 0;
final Uint8List _buffer;
BufferReader(Uint8List data) : _buffer = Uint8List.fromList(data);
int get remaining => _buffer.length - _pointer;
int readByte() => readBytes(1)[0];
Uint8List readBytes(int count) {
_lastPointer = _pointer;
if (_pointer + count > _buffer.length) {
throw RangeError(
'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
);
}
final data = _buffer.sublist(_pointer, _pointer + count);
_pointer += count;
return data;
}
void skipBytes(int count) {
_lastPointer = _pointer;
if (_pointer + count > _buffer.length) {
throw RangeError(
'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
);
}
_pointer += count;
}
Uint8List readRemainingBytes() => readBytes(remaining);
String readCStringGreedy(int maxLength) {
_lastPointer = _pointer;
final value = <int>[];
final bytes = readBytes(maxLength);
for (final byte in bytes) {
if (byte == 0) break;
value.add(byte);
}
try {
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
} catch (e) {
return String.fromCharCodes(value); // Latin-1 fallback
}
}
String readCString({int maxLength = -1}) {
final backupPointer = _pointer;
final value = <int>[];
int counter = 0;
final maxLen = maxLength >= 0 ? maxLength : remaining;
while (counter < maxLen) {
final byte = readByte();
if (byte == 0) break;
value.add(byte);
counter++;
}
_lastPointer = backupPointer;
try {
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
} catch (e) {
return String.fromCharCodes(value); // Latin-1 fallback
}
}
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
int readUInt16LE() =>
readBytes(2).buffer.asByteData().getUint16(0, Endian.little);
int readUInt16BE() =>
readBytes(2).buffer.asByteData().getUint16(0, Endian.big);
int readUInt32LE() =>
readBytes(4).buffer.asByteData().getUint32(0, Endian.little);
int readUInt32BE() =>
readBytes(4).buffer.asByteData().getUint32(0, Endian.big);
int readInt16LE() =>
readBytes(2).buffer.asByteData().getInt16(0, Endian.little);
int readInt16BE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.big);
int readInt32LE() =>
readBytes(4).buffer.asByteData().getInt32(0, Endian.little);
int readInt24BE() {
var value = (readByte() << 16) | (readByte() << 8) | readByte();
if ((value & 0x800000) != 0) value -= 0x1000000;
return value;
}
void resetPointer() => _pointer = 0;
void rewind() => _pointer = _lastPointer;
}
// Buffer Writer - accumulating binary data builder
class BufferWriter {
final BytesBuilder _builder = BytesBuilder();
Uint8List toBytes() => _builder.toBytes();
void writeByte(int byte) => _builder.addByte(byte);
void writeBytes(Uint8List bytes) => _builder.add(bytes);
void writeUInt16LE(int num) {
final bytes = Uint8List(2)
..buffer.asByteData().setUint16(0, num, Endian.little);
writeBytes(bytes);
}
void writeUInt32LE(int num) {
final bytes = Uint8List(4)
..buffer.asByteData().setUint32(0, num, Endian.little);
writeBytes(bytes);
}
void writeInt32LE(int num) {
final bytes = Uint8List(4)
..buffer.asByteData().setInt32(0, num, Endian.little);
writeBytes(bytes);
}
void writeString(String string) =>
writeBytes(Uint8List.fromList(utf8.encode(string)));
void writeCString(String string, int maxLength) {
final bytes = Uint8List(maxLength);
final encoded = utf8.encode(string);
for (var i = 0; i < maxLength - 1 && i < encoded.length; i++) {
bytes[i] = encoded[i];
}
writeBytes(bytes);
}
void writeHex(String hex) {
writeBytes(hex2Uint8List(hex));
}
void writeBytesPadded(Uint8List bytes, int totalLength) {
// Path data (64 bytes, zero-padded)
final bytesPadded = Uint8List(totalLength);
final len = bytes.length < totalLength ? bytes.length : totalLength;
if (bytes.isNotEmpty && len > 0) {
final copyLen = bytes.length < totalLength ? bytes.length : totalLength;
for (int i = 0; i < copyLen; i++) {
bytesPadded[i] = bytes[i];
}
}
writeBytes(bytesPadded);
}
}
Uint8List hex2Uint8List(String hex) {
// Validate hex string length is even and not empty
if (hex.isEmpty || hex.length % 2 != 0) {
throw FormatException('Invalid hex string length: ${hex.length}');
}
List<int> result = [];
for (int i = 0; i < hex.length ~/ 2; i++) {
final hexByte = hex.substring(i * 2, i * 2 + 2);
final byte = int.tryParse(hexByte, radix: 16);
if (byte == null) {
throw FormatException('Invalid hex characters at position $i: $hexByte');
}
result.add(byte);
}
return Uint8List.fromList(result);
}
// Command codes (to device) // Command codes (to device)
const int cmdAppStart = 1; const int cmdAppStart = 1;
const int cmdSendTxtMsg = 2; const int cmdSendTxtMsg = 2;
@ -28,18 +200,29 @@ const int cmdSendStatusReq = 27;
const int cmdGetContactByKey = 30; const int cmdGetContactByKey = 30;
const int cmdGetChannel = 31; const int cmdGetChannel = 31;
const int cmdSetChannel = 32; const int cmdSetChannel = 32;
const int cmdGetRadioSettings = 57; const int cmdSendTracePath = 36;
const int cmdSetOtherParams = 38;
const int cmdSendTelemetryReq = 39;
const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50;
const int cmdGetStats = 56;
const int cmdSendAnonReq = 57;
const int cmdSetAutoAddConfig = 58;
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;
const int reqTypeKeepAlive = 0x02; const int reqTypeKeepAlive = 0x02;
const int reqTypeGetTelemetry = 0x03; const int reqTypeGetTelemetry = 0x03;
const int reqTypeGetAccessList = 0x05; const int reqTypeGetAccessList = 0x05;
const int reqTypeGetNeighbours = 0x06; const int reqTypeGetNeighbors = 0x06;
// Repeater response codes // Repeater response codes
const int respServerLoginOk = 0; const int respServerLoginOk = 0;
@ -56,12 +239,19 @@ const int respCodeContactMsgRecv = 7;
const int respCodeChannelMsgRecv = 8; const int respCodeChannelMsgRecv = 8;
const int respCodeCurrTime = 9; const int respCodeCurrTime = 9;
const int respCodeNoMoreMessages = 10; const int respCodeNoMoreMessages = 10;
const int respCodeExportContact = 11;
const int respCodeBattAndStorage = 12; const int respCodeBattAndStorage = 12;
const int respCodeDeviceInfo = 13; const int respCodeDeviceInfo = 13;
const int respCodeContactMsgRecvV3 = 16; const int respCodeContactMsgRecvV3 = 16;
const int respCodeChannelMsgRecvV3 = 17; const int respCodeChannelMsgRecvV3 = 17;
const int respCodeChannelInfo = 18; const int respCodeChannelInfo = 18;
const int respCodeRadioSettings = 25; const int respCodeCustomVars = 21;
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;
@ -72,7 +262,10 @@ const int pushCodeLoginSuccess = 0x85;
const int pushCodeLoginFail = 0x86; const int pushCodeLoginFail = 0x86;
const int pushCodeStatusResponse = 0x87; const int pushCodeStatusResponse = 0x87;
const int pushCodeLogRxData = 0x88; const int pushCodeLogRxData = 0x88;
const int pushCodeTraceData = 0x89;
const int pushCodeNewAdvert = 0x8A; const int pushCodeNewAdvert = 0x8A;
const int pushCodeTelemetryResponse = 0x8B;
const int pushCodeBinaryResponse = 0x8C;
// Contact/advertisement types // Contact/advertisement types
const int advTypeChat = 1; const int advTypeChat = 1;
@ -80,8 +273,49 @@ 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
const int payloadTypeREQ =
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
const int payloadTypeRESPONSE =
0x01; // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
const int payloadTypeTXTMSG =
0x02; // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)
const int payloadTypeACK = 0x03; // a simple ack
const int payloadTypeADVERT = 0x04; // a node advertising its Identity
const int payloadTypeGRPTXT =
0x05; // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg")
const int payloadTypeGRPDATA =
0x06; // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob)
const int payloadTypeANONREQ =
0x07; // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
const int payloadTypePATH =
0x08; // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
const int payloadTypeTRACE = 0x09; // trace a path, collecting SNI for each hop
const int payloadTypeMULTIPART = 0x0A; // packet is one of a set of packets
const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
//...
const int payloadTypeRawCustom =
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
//auto-add flags
const int autoAddOverwriteOldestFlag =
1 << 0; // 0x01 - overwrite oldest non-favourite when full
const int autoAddChatFlag =
1 << 1; // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT)
const int autoAddRepeaterFlag =
1 << 2; // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER)
const int autoAddRoomServerFlag =
1 << 3; // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
const int autoAddSensorFlag =
1 << 4; // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
// 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;
@ -89,8 +323,10 @@ const int maxFrameSize = 172;
const int appProtocolVersion = 3; const int appProtocolVersion = 3;
// 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 = 1 + 1 + 1 + 4 + 6 + 1; const int _sendTextMsgOverheadBytes =
const int _sendChannelTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 1; 1 + 1 + 1 + 4 + 6 + 1 + 2; // +2 safety margin
const int _sendChannelTextMsgOverheadBytes =
1 + 1 + 1 + 4 + 1 + 2; // +2 safety margin
int maxContactMessageBytes() { int maxContactMessageBytes() {
final byFrame = maxFrameSize - _sendTextMsgOverheadBytes; final byFrame = maxFrameSize - _sendTextMsgOverheadBytes;
@ -121,13 +357,17 @@ int _minPositive(int a, int b) {
const int contactPubKeyOffset = 1; 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 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;
const int contactTimestampOffset = 132; const int contactTimestampOffset = 132;
const int contactLatOffset = 136; const int contactLatOffset = 136;
const int contactLonOffset = 140; const int contactLonOffset = 140;
const int contactLastmodOffset = 144; const int contactLastModOffset = 144;
const int contactFrameSize = 148; const int contactFrameSize = 148;
// Message frame offsets // Message frame offsets
@ -139,48 +379,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
@ -203,31 +439,9 @@ int readInt32LE(Uint8List data, int offset) {
return val; return val;
} }
// Helper to write uint32 little-endian // Helper to convert uint32 to hex string
void writeUint32LE(Uint8List data, int offset, int value) { String ackHashToHex(int ackHash) {
data[offset] = value & 0xFF; return ackHash.toRadixString(16).padLeft(8, '0');
data[offset + 1] = (value >> 8) & 0xFF;
data[offset + 2] = (value >> 16) & 0xFF;
data[offset + 3] = (value >> 24) & 0xFF;
}
// Helper to write int32 little-endian
void writeInt32LE(Uint8List data, int offset, int value) {
writeUint32LE(data, offset, value & 0xFFFFFFFF);
}
// Helper to read null-terminated UTF-8 string
String readCString(Uint8List data, int offset, int maxLen) {
int end = offset;
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
@ -246,34 +460,32 @@ Uint8List hexToPubKey(String hex) {
// Build CMD_GET_CONTACTS frame // Build CMD_GET_CONTACTS frame
Uint8List buildGetContactsFrame({int? since}) { Uint8List buildGetContactsFrame({int? since}) {
final writer = BufferWriter();
writer.writeByte(cmdGetContacts);
if (since != null) { if (since != null) {
final frame = Uint8List(5); writer.writeUInt32LE(since);
frame[0] = cmdGetContacts;
writeUint32LE(frame, 1, since);
return frame;
} }
return Uint8List.fromList([cmdGetContacts]); return writer.toBytes();
} }
// Build CMD_SEND_LOGIN frame // Build CMD_SEND_LOGIN frame
// Format: [cmd][pub_key x32][password...]\0 // Format: [cmd][pub_key x32][password...]\0
Uint8List buildSendLoginFrame(Uint8List recipientPubKey, String password) { Uint8List buildSendLoginFrame(Uint8List recipientPubKey, String password) {
final passwordBytes = utf8.encode(password); final writer = BufferWriter();
final frame = Uint8List(1 + pubKeySize + passwordBytes.length + 1); writer.writeByte(cmdSendLogin);
frame[0] = cmdSendLogin; writer.writeBytes(recipientPubKey);
frame.setRange(1, 1 + pubKeySize, recipientPubKey); writer.writeString(password);
frame.setRange(1 + pubKeySize, 1 + pubKeySize + passwordBytes.length, passwordBytes); writer.writeByte(0);
frame[frame.length - 1] = 0; return writer.toBytes();
return frame;
} }
// Build CMD_SEND_STATUS_REQ frame // Build CMD_SEND_STATUS_REQ frame
// Format: [cmd][pub_key x32] // Format: [cmd][pub_key x32]
Uint8List buildSendStatusRequestFrame(Uint8List recipientPubKey) { Uint8List buildSendStatusRequestFrame(Uint8List recipientPubKey) {
final frame = Uint8List(1 + pubKeySize); final writer = BufferWriter();
frame[0] = cmdSendStatusReq; writer.writeByte(cmdSendStatusReq);
frame.setRange(1, 1 + pubKeySize, recipientPubKey); writer.writeBytes(recipientPubKey);
return frame; return writer.toBytes();
} }
// Build CMD_SEND_TXT_MSG frame (companion_radio format) // Build CMD_SEND_TXT_MSG frame (companion_radio format)
@ -284,48 +496,39 @@ Uint8List buildSendTextMsgFrame(
int attempt = 0, int attempt = 0,
int? timestampSeconds, int? timestampSeconds,
}) { }) {
final textBytes = utf8.encode(text); final timestamp =
final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000); timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
const prefixSize = 6; final writer = BufferWriter();
final safeAttempt = attempt.clamp(0, 3); writer.writeByte(cmdSendTxtMsg);
final frame = Uint8List(1 + 1 + 1 + 4 + prefixSize + textBytes.length + 1); writer.writeByte(txtTypePlain);
int offset = 0; writer.writeByte(attempt.clamp(0, 255));
writer.writeUInt32LE(timestamp);
frame[offset++] = cmdSendTxtMsg; writer.writeBytes(recipientPubKey.sublist(0, 6));
frame[offset++] = txtTypePlain; writer.writeString(text);
frame[offset++] = safeAttempt; writer.writeByte(0);
writeUint32LE(frame, offset, timestamp); return writer.toBytes();
offset += 4;
frame.setRange(offset, offset + prefixSize, recipientPubKey.sublist(0, prefixSize));
offset += prefixSize;
frame.setRange(offset, offset + textBytes.length, textBytes);
frame[frame.length - 1] = 0; // null terminator
return frame;
} }
// Build CMD_SEND_CHANNEL_TXT_MSG frame // Build CMD_SEND_CHANNEL_TXT_MSG frame
// Format: [cmd][txt_type][channel_idx][timestamp x4][text...] // Format: [cmd][txt_type][channel_idx][timestamp x4][text...]
Uint8List buildSendChannelTextMsgFrame(int channelIndex, String text) { Uint8List buildSendChannelTextMsgFrame(int channelIndex, String text) {
final textBytes = utf8.encode(text);
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final frame = Uint8List(1 + 1 + 1 + 4 + textBytes.length + 1); final writer = BufferWriter();
frame[0] = cmdSendChannelTxtMsg; writer.writeByte(cmdSendChannelTxtMsg);
frame[1] = 0; // TXT_TYPE_PLAIN writer.writeByte(txtTypePlain);
frame[2] = channelIndex; writer.writeByte(channelIndex);
writeUint32LE(frame, 3, timestamp); writer.writeUInt32LE(timestamp);
frame.setRange(7, 7 + textBytes.length, textBytes); writer.writeString(text);
frame[frame.length - 1] = 0; // null terminator writer.writeByte(0);
return frame; return writer.toBytes();
} }
// Build CMD_REMOVE_CONTACT frame // Build CMD_REMOVE_CONTACT frame
Uint8List buildRemoveContactFrame(Uint8List pubKey) { Uint8List buildRemoveContactFrame(Uint8List pubKey) {
final frame = Uint8List(1 + pubKeySize); final writer = BufferWriter();
frame[0] = cmdRemoveContact; writer.writeByte(cmdRemoveContact);
frame.setRange(1, 1 + pubKeySize, pubKey); writer.writeBytes(pubKey);
return frame; return writer.toBytes();
} }
// Build CMD_APP_START frame // Build CMD_APP_START frame
@ -334,14 +537,13 @@ Uint8List buildAppStartFrame({
String appName = 'MeshCoreOpen', String appName = 'MeshCoreOpen',
int appVersion = 1, int appVersion = 1,
}) { }) {
final nameBytes = utf8.encode(appName); final writer = BufferWriter();
final frame = Uint8List(8 + nameBytes.length + 1); writer.writeByte(cmdAppStart);
frame[0] = cmdAppStart; writer.writeByte(appVersion);
frame[1] = appVersion; writer.writeBytes(Uint8List(6)); // reserved bytes
// bytes 2-7 are reserved (zeros) writer.writeString(appName);
frame.setRange(8, 8 + nameBytes.length, nameBytes); writer.writeByte(0);
frame[frame.length - 1] = 0; // null terminator return writer.toBytes();
return frame;
} }
// Build CMD_DEVICE_QUERY frame // Build CMD_DEVICE_QUERY frame
@ -359,12 +561,23 @@ 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..2 (mode+1) bytes per hop hash.
Uint8List buildSetPathHashModeFrame(int mode) {
final m = mode.clamp(0, 2);
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 frame = Uint8List(5); final writer = BufferWriter();
frame[0] = cmdSetDeviceTime; writer.writeByte(cmdSetDeviceTime);
writeUint32LE(frame, 1, timestamp); writer.writeUInt32LE(timestamp);
return frame; return writer.toBytes();
} }
// Build CMD_SEND_SELF_ADVERT frame // Build CMD_SEND_SELF_ADVERT frame
@ -377,21 +590,31 @@ Uint8List buildSendSelfAdvertFrame({bool flood = false}) {
// Format: [cmd][name...] // Format: [cmd][name...]
Uint8List buildSetAdvertNameFrame(String name) { Uint8List buildSetAdvertNameFrame(String name) {
final nameBytes = utf8.encode(name); final nameBytes = utf8.encode(name);
final nameLen = nameBytes.length < maxNameSize ? nameBytes.length : maxNameSize - 1; final nameLen = nameBytes.length < maxNameSize
final frame = Uint8List(1 + nameLen); ? nameBytes.length
frame[0] = cmdSetAdvertName; : maxNameSize - 1;
frame.setRange(1, 1 + nameLen, nameBytes.sublist(0, nameLen)); final writer = BufferWriter();
return frame; writer.writeByte(cmdSetAdvertName);
writer.writeBytes(Uint8List.fromList(nameBytes.sublist(0, nameLen)));
return writer.toBytes();
} }
// Build CMD_SET_ADVERT_LATLON frame // Build CMD_SET_ADVERT_LATLON frame
// Format: [cmd][lat x4][lon x4] // Format: [cmd][lat x4][lon x4]
Uint8List buildSetAdvertLatLonFrame(double lat, double lon) { Uint8List buildSetAdvertLatLonFrame(double lat, double lon) {
final frame = Uint8List(9); final writer = BufferWriter();
frame[0] = cmdSetAdvertLatLon; writer.writeByte(cmdSetAdvertLatLon);
writeInt32LE(frame, 1, (lat * 1000000).round()); writer.writeInt32LE((lat * 1000000).round());
writeInt32LE(frame, 5, (lon * 1000000).round()); writer.writeInt32LE((lon * 1000000).round());
return frame; return writer.toBytes();
}
Uint8List buildSetCustomVarFrame(String value) {
final writer = BufferWriter();
writer.writeByte(cmdSetCustomVar);
writer.writeString(value);
writer.writeByte(0);
return writer.toBytes();
} }
// Build CMD_REBOOT frame // Build CMD_REBOOT frame
@ -413,37 +636,44 @@ Uint8List buildGetChannelFrame(int channelIndex) {
// Build CMD_SET_CHANNEL frame // Build CMD_SET_CHANNEL frame
// Format: [cmd][idx][name x32][psk x16] // Format: [cmd][idx][name x32][psk x16]
Uint8List buildSetChannelFrame(int channelIndex, String name, Uint8List psk) { Uint8List buildSetChannelFrame(int channelIndex, String name, Uint8List psk) {
final frame = Uint8List(2 + 32 + 16); final writer = BufferWriter();
frame[0] = cmdSetChannel; writer.writeByte(cmdSetChannel);
frame[1] = channelIndex; writer.writeByte(channelIndex);
// Write name (max 32 bytes UTF-8, null-padded) writer.writeCString(name, 32);
final nameBytes = utf8.encode(name); // Write PSK (16 bytes, zero-padded)
final nameLen = nameBytes.length < 32 ? nameBytes.length : 31; // Reserve 1 byte for null final pskPadded = Uint8List(16);
for (int i = 0; i < nameLen; i++) {
frame[2 + i] = nameBytes[i];
}
// frame[2 + nameLen] is already 0 (null terminator)
// Write PSK (16 bytes)
for (int i = 0; i < 16 && i < psk.length; i++) { for (int i = 0; i < 16 && i < psk.length; i++) {
frame[34 + i] = psk[i]; pskPadded[i] = psk[i];
} }
return frame; writer.writeBytes(pskPadded);
return writer.toBytes();
} }
// Build CMD_SET_RADIO_PARAMS frame // Build CMD_SET_RADIO_PARAMS frame
// Format: [cmd][freq x4][bw x4][sf][cr] // Format: [cmd][freq x4][bw x4][sf][cr] (pre-v9)
// [cmd][freq x4][bw x4][sf][cr][repeat] (firmware v9+)
// freq: frequency in Hz (300000-2500000) // freq: frequency in Hz (300000-2500000)
// bw: bandwidth in Hz (7000-500000) // bw: bandwidth in Hz (7000-500000)
// sf: spreading factor (5-12) // sf: spreading factor (5-12)
// cr: coding rate (5-8) // cr: coding rate (5-8)
Uint8List buildSetRadioParamsFrame(int freqHz, int bwHz, int sf, int cr) { // clientRepeat: enable off-grid packet repeat (firmware v9+, omit for older)
final frame = Uint8List(11); Uint8List buildSetRadioParamsFrame(
frame[0] = cmdSetRadioParams; int freqHz,
writeUint32LE(frame, 1, freqHz); int bwHz,
writeUint32LE(frame, 5, bwHz); int sf,
frame[9] = sf; int cr, {
frame[10] = cr; bool? clientRepeat,
return frame; }) {
final writer = BufferWriter();
writer.writeByte(cmdSetRadioParams);
writer.writeUInt32LE(freqHz);
writer.writeUInt32LE(bwHz);
writer.writeByte(sf);
writer.writeByte(cr);
if (clientRepeat != null) {
writer.writeByte(clientRepeat ? 1 : 0);
}
return writer.toBytes();
} }
// Build CMD_SET_RADIO_TX_POWER frame // Build CMD_SET_RADIO_TX_POWER frame
@ -455,71 +685,81 @@ Uint8List buildSetRadioTxPowerFrame(int powerDbm) {
// Build CMD_RESET_PATH frame // Build CMD_RESET_PATH frame
// Format: [cmd][pub_key x32] // Format: [cmd][pub_key x32]
Uint8List buildResetPathFrame(Uint8List pubKey) { Uint8List buildResetPathFrame(Uint8List pubKey) {
final frame = Uint8List(1 + pubKeySize); final writer = BufferWriter();
frame[0] = cmdResetPath; writer.writeByte(cmdResetPath);
frame.setRange(1, 1 + pubKeySize, pubKey); writer.writeBytes(pubKey);
return frame; return writer.toBytes();
} }
// Build CMD_ADD_UPDATE_CONTACT frame to set custom path // Build CMD_ADD_UPDATE_CONTACT frame to set custom path
// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][timestamp x4] // Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][Lat? x4, Lon? x4][timestamp? x4]
Uint8List buildUpdateContactPathFrame( Uint8List buildUpdateContactPathFrame(
Uint8List pubKey, Uint8List pubKey,
Uint8List customPath, Uint8List path,
int pathLen, { int pathLen, {
int type = 1, // ADV_TYPE_CHAT int type = 1, // ADV_TYPE_CHAT
int flags = 0, int flags = 0,
String name = '', String name = '',
double? lat,
double? lon,
DateTime? lastModified,
}) { }) {
// Frame size: 1 + 32 + 1 + 1 + 1 + 64 + 32 + 4 = 136 bytes minimum final writer = BufferWriter();
final frame = Uint8List(1 + pubKeySize + 1 + 1 + 1 + maxPathSize + maxNameSize + 4); writer.writeByte(cmdAddUpdateContact);
int offset = 0; writer.writeBytes(pubKey);
writer.writeByte(type);
writer.writeByte(flags);
writer.writeByte(pathLen);
frame[offset++] = cmdAddUpdateContact; writer.writeBytesPadded(path, maxPathSize);
// Public key (32 bytes)
frame.setRange(offset, offset + pubKeySize, pubKey);
offset += pubKeySize;
// Type and flags
frame[offset++] = type;
frame[offset++] = flags;
// Path length and path data
frame[offset++] = pathLen;
if (customPath.isNotEmpty && pathLen > 0) {
final copyLen = customPath.length < maxPathSize ? customPath.length : maxPathSize;
frame.setRange(offset, offset + copyLen, customPath.sublist(0, copyLen));
}
offset += maxPathSize;
// Name (32 bytes, null-padded) // Name (32 bytes, null-padded)
if (name.isNotEmpty) { writer.writeCString(name, maxNameSize);
final nameBytes = utf8.encode(name);
final nameLen = nameBytes.length < maxNameSize ? nameBytes.length : maxNameSize - 1;
frame.setRange(offset, offset + nameLen, nameBytes.sublist(0, nameLen));
}
offset += maxNameSize;
// Timestamp (current time) // Timestamp
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
writeUint32LE(frame, offset, timestamp); writer.writeUInt32LE(timestamp);
return frame; if ((lat == null || lon == null) && lastModified != null) {
// If lat/lon not provided, write zeros
writer.writeInt32LE(0);
writer.writeInt32LE(0);
} else {
// Latitude and Longitude are expected in degrees, convert to int by multiplying by 1e6
// Latitude
final latitude = lat ?? 0.0;
writer.writeInt32LE((latitude * 1e6).round());
// Longitude
final longitude = lon ?? 0.0;
writer.writeInt32LE((longitude * 1e6).round());
}
if (lastModified != null) {
// Last modified
final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000;
writer.writeUInt32LE(lastModifiedTimestamp);
}
return writer.toBytes();
} }
// Build CMD_GET_CONTACT_BY_KEY frame // Build CMD_GET_CONTACT_BY_KEY frame
// Format: [cmd][pub_key x32] // Format: [cmd][pub_key x32]
Uint8List buildGetContactByKeyFrame(Uint8List pubKey) { Uint8List buildGetContactByKeyFrame(Uint8List pubKey) {
final frame = Uint8List(1 + pubKeySize); final writer = BufferWriter();
frame[0] = cmdGetContactByKey; writer.writeByte(cmdGetContactByKey);
frame.setRange(1, 1 + pubKeySize, pubKey); writer.writeBytes(pubKey);
return frame; return writer.toBytes();
} }
// Build CMD_GET_RADIO_SETTINGS frame //Build CMD_GET_CUSTOM_VARS frame
Uint8List buildGetRadioSettingsFrame() { Uint8List buildGetCustomVarsFrame() {
return Uint8List.fromList([cmdGetRadioSettings]); return Uint8List.fromList([cmdGetCustomVar]);
}
Uint8List buildGetAutoAddFlagsFrame() {
return Uint8List.fromList([cmdGetAutoAddConfig]);
} }
// Calculate LoRa airtime for a packet // Calculate LoRa airtime for a packet
@ -545,9 +785,11 @@ int calculateLoRaAirtime({
final crc = 1; // CRC enabled final crc = 1; // CRC enabled
final de = lowDataRateOptimize ? 1 : 0; final de = lowDataRateOptimize ? 1 : 0;
final numerator = 8 * payloadBytes - 4 * spreadingFactor + 28 + 16 * crc - headerBytes; final numerator =
8 * payloadBytes - 4 * spreadingFactor + 28 + 16 * crc - headerBytes;
final denominator = 4 * (spreadingFactor - 2 * de); final denominator = 4 * (spreadingFactor - 2 * de);
var payloadSymbols = 8 + ((numerator / denominator).ceil()) * (codingRate + 4); var payloadSymbols =
8 + ((numerator / denominator).ceil()) * (codingRate + 4);
if (payloadSymbols < 0) { if (payloadSymbols < 0) {
payloadSymbols = 8; payloadSymbols = 8;
@ -594,23 +836,122 @@ Uint8List buildSendCliCommandFrame(
int attempt = 0, int attempt = 0,
int? timestampSeconds, int? timestampSeconds,
}) { }) {
final textBytes = utf8.encode(command); final timestamp =
final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000); timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
const prefixSize = 6; final writer = BufferWriter();
final safeAttempt = attempt.clamp(0, 3); writer.writeByte(cmdSendTxtMsg);
final frame = Uint8List(1 + 1 + 1 + 4 + prefixSize + textBytes.length + 1); writer.writeByte(txtTypeCliData);
int offset = 0; writer.writeByte(attempt.clamp(0, 255));
writer.writeUInt32LE(timestamp);
frame[offset++] = cmdSendTxtMsg; writer.writeBytes(repeaterPubKey.sublist(0, 6));
frame[offset++] = txtTypeCliData; writer.writeString(command);
frame[offset++] = safeAttempt; writer.writeByte(0);
writeUint32LE(frame, offset, timestamp); return writer.toBytes();
offset += 4; }
frame.setRange(offset, offset + prefixSize, repeaterPubKey.sublist(0, prefixSize)); // Build a telemetry request frame
offset += prefixSize; // Format: [cmd][pub_key x32][payload]
Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
frame.setRange(offset, offset + textBytes.length, textBytes); final writer = BufferWriter();
frame[frame.length - 1] = 0; // null terminator writer.writeByte(cmdSendBinaryReq);
return frame; writer.writeBytes(repeaterPubKey);
if (payload != null && payload.isNotEmpty) {
writer.writeBytes(payload);
}
return writer.toBytes();
}
//Build a trace request frame
//[cmd][tag x4][auth x4][flag][payload]
Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) {
final writer = BufferWriter();
writer.writeByte(cmdSendTracePath);
writer.writeUInt32LE(tag);
writer.writeUInt32LE(auth);
writer.writeByte(flag);
if (payload != null && payload.isNotEmpty) {
writer.writeBytes(payload);
}
return writer.toBytes();
}
// Build a export contact frame
// [cmd][pub_key x32 / if empty exports your contact info]
Uint8List buildExportContactFrame(Uint8List pubKey) {
final writer = BufferWriter();
writer.writeByte(cmdExportContact);
writer.writeBytes(pubKey);
return writer.toBytes();
}
// Build a import contact frame
// [cmd][contact_frame x98+]
Uint8List buildImportContactFrame(Uint8List contactFrame) {
final writer = BufferWriter();
writer.writeByte(cmdImportContact);
writer.writeBytes(contactFrame);
return writer.toBytes();
}
// Build a export contact frame
// [cmd][pub_key x32]
Uint8List buildZeroHopContact(Uint8List pubKey) {
final writer = BufferWriter();
writer.writeByte(cmdShareContact);
writer.writeBytes(pubKey);
return writer.toBytes();
}
// Build CMD_SET_OTHER_PARAMS frame
// Format: [cmd][allowTelemetryFlags][advertLocationPolicy][multiAcks]
Uint8List buildSetOtherParamsFrame(
int allowTelemetryFlags,
int advertLocationPolicy,
int multiAcks,
) {
final writer = BufferWriter();
writer.writeByte(cmdSetOtherParams);
//Going forward the app will just set Auto Add Contacts to disabled, and use the filter flags
//Allow Auto Add Contacts use inverted logic (0x01 = disabled, 0x00 = enabled).
writer.writeByte(0x01);
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
writer.writeByte(multiAcks); // Multi Acknowledgements
return writer.toBytes();
}
// Build CMD_SET_AUTO_ADD_CONFIG frame
// Format: [cmd][flags]
Uint8List buildSetAutoAddConfigFrame({
required bool autoAddChat,
required bool autoAddRepeater,
required bool autoAddRoomServer,
required bool autoAddSensor,
required bool overwriteOldest,
}) {
final writer = BufferWriter();
writer.writeByte(cmdSetAutoAddConfig);
int flags = 0;
if (autoAddChat) flags |= autoAddChatFlag;
if (autoAddRepeater) flags |= autoAddRepeaterFlag;
if (autoAddRoomServer) flags |= autoAddRoomServerFlag;
if (autoAddSensor) flags |= autoAddSensorFlag;
if (overwriteOldest) flags |= autoAddOverwriteOldestFlag;
writer.writeByte(flags);
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(4)); // reserved bytes
}
return writer.toBytes();
} }

View file

@ -0,0 +1,15 @@
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";
static const List<String> deviceNamePrefixes = [
"MeshCore-",
"Whisper-",
"WisCore-",
"Seeed",
"Lilygo",
"HT-",
"LowMesh_MC_",
];
}

View file

@ -0,0 +1,277 @@
import 'dart:typed_data';
import 'package:meshcore_open/utils/app_logger.dart';
import '../connector/meshcore_protocol.dart';
class CayenneLpp {
static const int lppDigitalInput = 0; // 1 byte
static const int lppDigitalOutput = 1; // 1 byte
static const int lppAnalogInput = 2; // 2 bytes, 0.01 signed
static const int lppAnalogOutput = 3; // 2 bytes, 0.01 signed
static const int lppGenericSensor = 100; // 4 bytes, unsigned
static const int lppLuminosity = 101; // 2 bytes, 1 lux unsigned
static const int lppPresence = 102; // 1 byte, bool
static const int lppTemperature = 103; // 2 bytes, 0.1°C signed
static const int lppRelativeHumidity = 104; // 1 byte, 0.5% unsigned
static const int lppAccelerometer = 113; // 2 bytes per axis, 0.001G
static const int lppBarometricPressure = 115; // 2 bytes 0.1hPa unsigned
static const int lppVoltage = 116; // 2 bytes 0.01V unsigned
static const int lppCurrent = 117; // 2 bytes 0.001A unsigned
static const int lppFrequency = 118; // 4 bytes 1Hz unsigned
static const int lppPercentage = 120; // 1 byte 1-100% unsigned
static const int lppAltitude = 121; // 2 byte 1m signed
static const int lppConcentration = 125; // 2 bytes, 1 ppm unsigned
static const int lppPower = 128; // 2 byte, 1W, unsigned
static const int lppDistance = 130; // 4 byte, 0.001m, unsigned
static const int lppEnergy = 131; // 4 byte, 0.001kWh, unsigned
static const int lppDirection = 132; // 2 bytes, 1deg, unsigned
static const int lppUnixTime = 133; // 4 bytes, unsigned
static const int lppGyrometer = 134; // 2 bytes per axis, 0.01 °/s
static const int lppColour = 135; // 1 byte per RGB Color
static const int lppGps =
136; // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter
static const int lppSwitch = 142; // 1 byte, 0/1
static const int lppPolyline =
240; // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas
final BufferWriter _writer = BufferWriter();
Uint8List toBytes() {
return _writer.toBytes();
}
void addDigitalInput(int channel, int value) {
_writer.writeByte(channel);
_writer.writeByte(lppDigitalInput);
_writer.writeByte(value);
}
void addTemperature(int channel, double value) {
_writer.writeByte(channel);
_writer.writeByte(lppTemperature);
final val = (value * 10).toInt();
_writer.writeBytes(_int16ToBE(val));
}
void addVoltage(int channel, double value) {
_writer.writeByte(channel);
_writer.writeByte(lppVoltage);
final val = (value * 100).toInt();
_writer.writeBytes(_int16ToBE(val));
}
void addGps(int channel, double lat, double lon, double alt) {
_writer.writeByte(channel);
_writer.writeByte(lppGps);
_writer.writeBytes(_int24ToBE((lat * 10000).toInt()));
_writer.writeBytes(_int24ToBE((lon * 10000).toInt()));
_writer.writeBytes(_int24ToBE((alt * 100).toInt()));
}
Uint8List _int16ToBE(int value) {
final bytes = Uint8List(2);
final data = ByteData.view(bytes.buffer);
data.setInt16(0, value, Endian.big);
return bytes;
}
Uint8List _int24ToBE(int value) {
final bytes = Uint8List(3);
bytes[0] = (value >> 16) & 0xFF;
bytes[1] = (value >> 8) & 0xFF;
bytes[2] = value & 0xFF;
return bytes;
}
static List<Map<String, dynamic>> parse(Uint8List bytes) {
final buffer = BufferReader(bytes);
final telemetry = <Map<String, dynamic>>[];
try {
while (buffer.remaining >= 2) {
final channel = buffer.readUInt8();
final type = buffer.readUInt8();
if (channel == 0 && type == 0) {
break;
}
switch (type) {
case lppGenericSensor:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt32BE(),
});
break;
case lppLuminosity:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppPresence:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8(),
});
break;
case lppTemperature:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 10,
});
break;
case lppRelativeHumidity:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8() / 2,
});
break;
case lppBarometricPressure:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE() / 10,
});
break;
case lppVoltage:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 100,
});
break;
case lppCurrent:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readInt16BE() / 1000,
});
break;
case lppPercentage:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt8(),
});
break;
case lppConcentration:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppPower:
telemetry.add({
'channel': channel,
'type': type,
'value': buffer.readUInt16BE(),
});
break;
case lppGps:
telemetry.add({
'channel': channel,
'type': type,
'value': {
'latitude': buffer.readInt24BE() / 10000,
'longitude': buffer.readInt24BE() / 10000,
'altitude': buffer.readInt24BE() / 100,
},
});
break;
default:
return telemetry;
}
}
return telemetry;
} catch (e) {
// Handle parsing errors, possibly due to malformed data
appLogger.error('Error parsing Cayenne LPP data: $e');
// Return any telemetry parsed so far to preserve partial data
return telemetry;
}
}
static List<Map<String, dynamic>> parseByChannel(Uint8List bytes) {
final buffer = BufferReader(bytes);
final Map<int, Map<String, dynamic>> channels = {};
try {
while (buffer.remaining >= 2) {
final channel = buffer.readUInt8();
final type = buffer.readUInt8();
// Optional: stop on padding (00 00)
if (channel == 0 && type == 0) {
break;
}
final channelData = channels.putIfAbsent(
channel,
() => {'channel': channel, 'values': <String, dynamic>{}},
);
switch (type) {
case lppGenericSensor:
channelData['values']['generic'] = buffer.readUInt32BE();
break;
case lppLuminosity:
channelData['values']['luminosity'] = buffer.readUInt16BE();
break;
case lppPresence:
channelData['values']['presence'] = buffer.readUInt8() != 0;
break;
case lppTemperature:
channelData['values']['temperature'] = buffer.readInt16BE() / 10.0;
break;
case lppRelativeHumidity:
channelData['values']['humidity'] = buffer.readUInt8() / 2.0;
break;
case lppBarometricPressure:
channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0;
break;
case lppVoltage:
channelData['values']['voltage'] = buffer.readInt16BE() / 100.0;
break;
case lppCurrent:
channelData['values']['current'] = buffer.readInt16BE() / 1000.0;
break;
case lppPercentage:
channelData['values']['percentage'] = buffer.readUInt8();
break;
case lppConcentration:
channelData['values']['concentration'] = buffer.readUInt16BE();
break;
case lppPower:
channelData['values']['power'] = buffer.readUInt16BE();
break;
case lppGps:
channelData['values']['gps'] = {
'latitude': buffer.readInt24BE() / 10000.0,
'longitude': buffer.readInt24BE() / 10000.0,
'altitude': buffer.readInt24BE() / 100.0,
};
break;
// Add more types as needed...
default:
//Stopped parsing to avoid misalignment
return channels.values.toList();
}
}
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
return channelsOut;
} catch (e) {
// Handle parsing errors, possibly due to malformed data
appLogger.error('Error parsing Cayenne LPP data: $e');
return <
Map<String, dynamic>
>[]; // Return an empty list on error to avoid crashing the app
}
}
}

View file

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
class ChatScrollController extends ScrollController {
final ValueNotifier<bool> showJumpToBottom = ValueNotifier(false);
VoidCallback? onScrollNearTop;
static const _bottomThreshold = 100.0;
static const _topThreshold = 50.0;
ChatScrollController() {
addListener(_handleScroll);
}
void _handleScroll() {
if (!hasClients) return;
final pos = position;
// With reverse: true, position 0 is bottom, maxScrollExtent is top
// Show jump button when scrolled away from bottom (position > threshold)
final isAtBottom = pos.pixels <= _bottomThreshold;
if (showJumpToBottom.value == isAtBottom) {
showJumpToBottom.value = !isAtBottom;
}
// Pagination trigger when scrolled near top (maxScrollExtent)
if (pos.pixels >= pos.maxScrollExtent - _topThreshold) {
onScrollNearTop?.call();
}
}
void jumpToBottom() {
if (hasClients && position.maxScrollExtent > 0) {
animateTo(
0, // With reverse: true, position 0 is bottom
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
void handleKeyboardOpen() {
// Simple: just scroll to bottom when keyboard opens
if (hasClients) {
animateTo(
0, // With reverse: true, position 0 is bottom
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
}
void scrollToBottomIfAtBottom() {
// Only scroll if jump button is NOT showing (i.e., already at bottom)
if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) {
animateTo(
0, // With reverse: true, position 0 is bottom
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
}
@override
void dispose() {
showJumpToBottom.dispose();
super.dispose();
}
}

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';
}
}

View file

@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart';
import '../l10n/l10n.dart';
import '../utils/platform_info.dart';
import '../helpers/snack_bar_builder.dart';
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,
}) {
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) {
return SelectableLinkify(
text: text,
style: style,
linkStyle: effectiveLinkStyle,
options: options,
linkifiers: linkifiers,
onOpen: onOpen,
);
}
return Linkify(
text: text,
style: style,
linkStyle: effectiveLinkStyle,
options: options,
linkifiers: linkifiers,
onOpen: onOpen,
);
}
static Future<void> handleLinkTap(BuildContext context, String url) async {
// Show confirmation dialog
final shouldOpen = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.chat_openLink),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.chat_openLinkConfirmation,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: SelectableText(
url,
style: const TextStyle(fontSize: 12, fontFamily: 'monospace'),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(context.l10n.common_cancel),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: Text(context.l10n.chat_open),
),
],
),
);
if (shouldOpen != true) return;
// Launch URL
try {
final uri = Uri.parse(url);
if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
if (context.mounted) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.chat_couldNotOpenLink(url)),
backgroundColor: Colors.red,
);
}
}
} catch (e) {
if (context.mounted) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.chat_invalidLink),
backgroundColor: Colors.red,
);
}
}
}
}

View file

@ -0,0 +1,31 @@
import '../models/contact.dart';
import '../connector/meshcore_protocol.dart';
class PathHelper {
static String formatPathHex(List<int> pathBytes) {
return pathBytes
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join(',');
}
static String resolvePathNames(
List<int> pathBytes,
List<Contact> allContacts,
) {
return pathBytes
.map((b) {
final hex = b.toRadixString(16).padLeft(2, '0').toUpperCase();
final matches = allContacts
.where(
(c) =>
c.publicKey.first == b &&
(c.type == advTypeRepeater || c.type == advTypeRoom),
)
.toList();
if (matches.isEmpty) return hex;
if (matches.length == 1) return matches.first.name;
return matches.map((c) => c.name).join(' | ');
})
.join(' \u2192 ');
}
}

View file

@ -1,53 +1,117 @@
class ReactionInfo { import '../widgets/emoji_picker.dart';
final String targetMessageId;
final String emoji;
final String? reactionKey; // Lightweight key for deduplication: timestamp_senderPrefix
ReactionInfo({ class ReactionInfo {
required this.targetMessageId, final String targetHash;
required this.emoji, final String emoji;
this.reactionKey,
}); ReactionInfo({required this.targetHash, required this.emoji});
} }
class ReactionHelper { class ReactionHelper {
/// Parse reaction format: r:[messageId]:[emoji] /// Apply a reaction to a list of messages by matching the reaction hash.
/// Supports both old format (full messageId) and new format (timestamp_senderPrefix) ///
/// [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;
/// Combined list of all reaction emojis in fixed order.
/// Order must stay stable for index compatibility.
static List<String> get reactionEmojis {
return _cachedEmojis ??= [
...EmojiPicker.quickEmojis,
...EmojiPicker.smileys,
...EmojiPicker.gestures,
...EmojiPicker.hearts,
...EmojiPicker.objects,
];
}
/// Convert emoji to 2-char hex index. Returns null if emoji not in list.
static String? emojiToIndex(String emoji) {
final idx = reactionEmojis.indexOf(emoji);
if (idx < 0) return null;
return idx.toRadixString(16).padLeft(2, '0');
}
/// Convert 2-char hex index to emoji. Returns null if invalid index.
static String? indexToEmoji(String hexIndex) {
final idx = int.tryParse(hexIndex, radix: 16);
if (idx == null || idx < 0 || idx >= reactionEmojis.length) return null;
return reactionEmojis[idx];
}
/// Compute a 4-char hex hash for a message reaction.
/// Hash input: timestampSeconds + [senderName] + first 5 chars of text
/// For 1:1 chats, senderName can be null (sender is implicit).
static String computeReactionHash(
int timestampSeconds,
String? senderName,
String text,
) {
final first5 = text.length >= 5 ? text.substring(0, 5) : text;
final input = senderName != null
? '$timestampSeconds$senderName$first5'
: '$timestampSeconds$first5';
// Use hashCode and take lower 16 bits, format as 4 hex chars
final hash = input.hashCode & 0xFFFF;
return hash.toRadixString(16).padLeft(4, '0');
}
/// Parse reaction format: r:HASH:INDEX (where INDEX is 2-char hex emoji index)
/// Returns null if text is not a valid reaction format
static ReactionInfo? parseReaction(String text) { static ReactionInfo? parseReaction(String text) {
final regex = RegExp(r'^r:([^:]+):(.+)$'); final regex = RegExp(r'^r:([0-9a-f]{4}):([0-9a-f]{2})$');
final match = regex.firstMatch(text); final match = regex.firstMatch(text);
if (match == null) return null; if (match == null) return null;
final targetId = match.group(1)!; final emoji = indexToEmoji(match.group(2)!);
final emoji = match.group(2)!; if (emoji == null) return null;
// Extract reaction key for deduplication return ReactionInfo(targetHash: match.group(1)!, emoji: emoji);
// If targetId is in new format (timestamp_senderPrefix), use it directly
// Otherwise, extract timestamp from old format (timestamp_nameHash_textHash)
String? reactionKey;
if (targetId.contains('_')) {
final parts = targetId.split('_');
if (parts.length >= 2) {
// New format: timestamp_senderPrefix, or old format with at least timestamp
reactionKey = '${parts[0]}_${parts[1]}';
}
}
return ReactionInfo(
targetMessageId: targetId,
emoji: emoji,
reactionKey: reactionKey,
);
} }
/// Generate a lightweight reaction key for a message /// Encode a reaction message that parseReaction() can parse.
/// Format: r:[timestamp]_[senderPrefix]:[emoji] static String encodeReaction(String hash, String emojiIndex) {
static String buildReactionText(String timestamp, String senderPrefix, String emoji) { return 'r:$hash:$emojiIndex';
return 'r:${timestamp}_$senderPrefix:$emoji';
}
/// Extract sender prefix from public key hex (first 8 chars)
static String getSenderPrefix(String senderKeyHex) {
return senderKeyHex.substring(0, 8);
} }
} }

View file

@ -262,8 +262,9 @@ class Smaz {
".com", ".com",
]; ];
static final List<Uint8List> _rcbBytes = static final List<Uint8List> _rcbBytes = _rcb
_rcb.map((s) => Uint8List.fromList(ascii.encode(s))).toList(growable: false); .map((s) => Uint8List.fromList(ascii.encode(s)))
.toList(growable: false);
static final int _maxEntryLen = _rcbBytes.fold(0, (maxLen, entry) { static final int _maxEntryLen = _rcbBytes.fold(0, (maxLen, entry) {
return entry.length > maxLen ? entry.length : maxLen; return entry.length > maxLen ? entry.length : maxLen;
}); });
@ -358,24 +359,32 @@ class Smaz {
final code = input[index]; final code = input[index];
if (code == _verbatimSingle) { if (code == _verbatimSingle) {
if (index + 1 >= input.length) { if (index + 1 >= input.length) {
throw const FormatException('Invalid SMAZ stream: truncated verbatim byte.'); throw const FormatException(
'Invalid SMAZ stream: truncated verbatim byte.',
);
} }
out.addByte(input[index + 1]); out.addByte(input[index + 1]);
index += 2; index += 2;
} else if (code == _verbatimRun) { } else if (code == _verbatimRun) {
if (index + 1 >= input.length) { if (index + 1 >= input.length) {
throw const FormatException('Invalid SMAZ stream: truncated verbatim length.'); throw const FormatException(
'Invalid SMAZ stream: truncated verbatim length.',
);
} }
final len = input[index + 1] + 1; final len = input[index + 1] + 1;
final end = index + 2 + len; final end = index + 2 + len;
if (end > input.length) { if (end > input.length) {
throw const FormatException('Invalid SMAZ stream: truncated verbatim run.'); throw const FormatException(
'Invalid SMAZ stream: truncated verbatim run.',
);
} }
out.add(input.sublist(index + 2, end)); out.add(input.sublist(index + 2, end));
index = end; index = end;
} else { } else {
if (code >= _rcbBytes.length) { if (code >= _rcbBytes.length) {
throw const FormatException('Invalid SMAZ stream: code out of range.'); throw const FormatException(
'Invalid SMAZ stream: code out of range.',
);
} }
out.add(_rcbBytes[code]); out.add(_rcbBytes[code]);
index += 1; index += 1;

View file

@ -0,0 +1,56 @@
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,
}) {
final messenger = ScaffoldMessenger.of(context);
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,
),
);
}

View file

@ -8,7 +8,10 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
const Utf8LengthLimitingTextInputFormatter(this.maxBytes); const Utf8LengthLimitingTextInputFormatter(this.maxBytes);
@override @override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
if (maxBytes <= 0) return oldValue; if (maxBytes <= 0) return oldValue;
final bytes = utf8.encode(newValue.text); final bytes = utf8.encode(newValue.text);
if (bytes.length <= maxBytes) return newValue; if (bytes.length <= maxBytes) return newValue;

22
lib/icons/los_icon.dart Normal file
View file

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
class LosIcon extends StatelessWidget {
final double size;
final Color? color;
const LosIcon({super.key, this.size = 24, this.color});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final iconTheme = IconTheme.of(context);
final iconColor =
color ??
iconTheme.color ??
theme.iconTheme.color ??
theme.colorScheme.onSurface;
return Icon(Symbols.elevation, size: size, color: iconColor);
}
}

2077
lib/l10n/app_bg.arb Normal file

File diff suppressed because it is too large Load diff

2105
lib/l10n/app_de.arb Normal file

File diff suppressed because it is too large Load diff

2125
lib/l10n/app_en.arb Normal file

File diff suppressed because it is too large Load diff

2105
lib/l10n/app_es.arb Normal file

File diff suppressed because it is too large Load diff

2077
lib/l10n/app_fr.arb Normal file

File diff suppressed because it is too large Load diff

2115
lib/l10n/app_hu.arb Normal file

File diff suppressed because it is too large Load diff

2077
lib/l10n/app_it.arb Normal file

File diff suppressed because it is too large Load diff

2115
lib/l10n/app_ja.arb Normal file

File diff suppressed because it is too large Load diff

2115
lib/l10n/app_ko.arb Normal 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

2077
lib/l10n/app_nl.arb Normal file

File diff suppressed because it is too large Load diff

2115
lib/l10n/app_pl.arb Normal file

File diff suppressed because it is too large Load diff

2077
lib/l10n/app_pt.arb Normal file

File diff suppressed because it is too large Load diff

1317
lib/l10n/app_ru.arb Normal file

File diff suppressed because it is too large Load diff

2077
lib/l10n/app_sk.arb Normal file

File diff suppressed because it is too large Load diff

2077
lib/l10n/app_sl.arb Normal file

File diff suppressed because it is too large Load diff

2077
lib/l10n/app_sv.arb Normal file

File diff suppressed because it is too large Load diff

2077
lib/l10n/app_uk.arb Normal file

File diff suppressed because it is too large Load diff

2082
lib/l10n/app_zh.arb Normal file

File diff suppressed because it is too large Load diff

6
lib/l10n/l10n.dart Normal file
View file

@ -0,0 +1,6 @@
import 'package:flutter/widgets.dart';
import 'app_localizations.dart';
extension LocalizationExtension on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this);
}

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