rebase dev over main and resolve merge conflicts
This commit is contained in:
Ded
2026-04-09 10:12:47 -07:00
committed by GitHub
28 changed files with 177 additions and 52 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
## BLE Frames & Protocol Notes ## BLE Frames & Protocol Notes
- Nordic UART Service (NUS) UUIDs: Service `6e400001-b5a3-f393-e0a9-e50e24dcca9e`, RX `6e400002-b5a3-f393-e0a9-e50e24dcca9e`, TX `6e400003-b5a3-f393-e0a9-e50e24dcca9e`. - Nordic UART Service (NUS) UUIDs: Service `6e400001-b5a3-f393-e0a9-e50e24dcca9e`, RX `6e400002-b5a3-f393-e0a9-e50e24dcca9e`, TX `6e400003-b5a3-f393-e0a9-e50e24dcca9e`.
- Discovery: scans for device name prefix `MeshCore-` and filters by `platformName`/`advertisementData.advName`. - Discovery: scans for device names matching known prefixes and filters by `platformName`/`advertisementData.advName`.
- Frames are capped at `maxFrameSize = 172` bytes; byte 0 is the command/response/push code. I/O is `MeshCoreConnector.sendFrame` and `MeshCoreConnector.receivedFrames`. - Frames are capped at `maxFrameSize = 172` bytes; byte 0 is the command/response/push code. I/O is `MeshCoreConnector.sendFrame` and `MeshCoreConnector.receivedFrames`.
- Command codes (to device): `cmdAppStart`=1, `cmdSendTxtMsg`=2, `cmdSendChannelTxtMsg`=3, `cmdGetContacts`=4, `cmdGetDeviceTime`=5, `cmdSetDeviceTime`=6, `cmdSendSelfAdvert`=7, `cmdSetAdvertName`=8, `cmdAddUpdateContact`=9, `cmdSyncNextMessage`=10, `cmdSetRadioParams`=11, `cmdSetRadioTxPower`=12, `cmdResetPath`=13, `cmdSetAdvertLatLon`=14, `cmdRemoveContact`=15, `cmdShareContact`=16, `cmdExportContact`=17, `cmdImportContact`=18, `cmdReboot`=19, `cmdSendLogin`=26, `cmdGetChannel`=31, `cmdSetChannel`=32, `cmdGetRadioSettings`=57. - Command codes (to device): `cmdAppStart`=1, `cmdSendTxtMsg`=2, `cmdSendChannelTxtMsg`=3, `cmdGetContacts`=4, `cmdGetDeviceTime`=5, `cmdSetDeviceTime`=6, `cmdSendSelfAdvert`=7, `cmdSetAdvertName`=8, `cmdAddUpdateContact`=9, `cmdSyncNextMessage`=10, `cmdSetRadioParams`=11, `cmdSetRadioTxPower`=12, `cmdResetPath`=13, `cmdSetAdvertLatLon`=14, `cmdRemoveContact`=15, `cmdShareContact`=16, `cmdExportContact`=17, `cmdImportContact`=18, `cmdReboot`=19, `cmdSendLogin`=26, `cmdGetChannel`=31, `cmdSetChannel`=32, `cmdGetRadioSettings`=57.
- Response codes (from device): `respCodeOk`=0, `respCodeErr`=1, `respCodeContactsStart`=2, `respCodeContact`=3, `respCodeEndOfContacts`=4, `respCodeSelfInfo`=5, `respCodeSent`=6, `respCodeContactMsgRecv`=7, `respCodeChannelMsgRecv`=8, `respCodeCurrTime`=9, `respCodeNoMoreMessages`=10, `respCodeContactMsgRecvV3`=16, `respCodeChannelMsgRecvV3`=17, `respCodeChannelInfo`=18, `respCodeRadioSettings`=25. - Response codes (from device): `respCodeOk`=0, `respCodeErr`=1, `respCodeContactsStart`=2, `respCodeContact`=3, `respCodeEndOfContacts`=4, `respCodeSelfInfo`=5, `respCodeSent`=6, `respCodeContactMsgRecv`=7, `respCodeChannelMsgRecv`=8, `respCodeCurrTime`=9, `respCodeNoMoreMessages`=10, `respCodeContactMsgRecvV3`=16, `respCodeChannelMsgRecvV3`=17, `respCodeChannelInfo`=18, `respCodeRadioSettings`=25.
+1 -1
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
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.
+11 -2
View File
@@ -150,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
@@ -184,7 +185,15 @@ lib/
### Device Discovery ### Device Discovery
Devices are discovered by scanning for BLE advertisements with the name prefix `MeshCore-` Devices are discovered by scanning for BLE advertisements with known MeshCore device name prefixes. These are currently:
- `MeshCore-`
- `Whisper-`
- `WisCore-`
- `HT-`
- `LowMesh_MC_`
New device prefixes can be added in `lib/connector/meshcore_uuids.dart`.
### Message Format ### Message Format
+6 -1
View File
@@ -21,7 +21,12 @@ The MeshCore BLE protocol implements a binary frame-based communication system u
### Connection Flow ### Connection Flow
1. **Scan** for devices with name prefix `MeshCore-` 1. **Scan** for devices with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`):
- `MeshCore-`
- `Whisper-`
- `WisCore-`
- `HT-`
- `LowMesh_MC_`
2. **Connect** with 15-second timeout 2. **Connect** with 15-second timeout
3. **Request MTU** of 185 bytes (falls back to default if unsupported) 3. **Request MTU** of 185 bytes (falls back to default if unsupported)
4. **Discover services** and locate NUS characteristics 4. **Discover services** and locate NUS characteristics
+6 -1
View File
@@ -49,7 +49,12 @@ enum MeshCoreConnectionState {
## BLE Connection Lifecycle ## BLE Connection Lifecycle
1. **Scan** with keyword filters `["MeshCore-", "Whisper-"]` 1. **Scan** with known name prefixes (defined in `MeshCoreUuids.deviceNamePrefixes`):
- `MeshCore-`
- `Whisper-`
- `WisCore-`
- `HT-`
- `LowMesh_MC_`
2. **Connect** with 15-second timeout 2. **Connect** with 15-second timeout
3. **Request MTU** 185 bytes (non-web only) 3. **Request MTU** 185 bytes (non-web only)
4. **Discover services** and locate NUS 4. **Discover services** and locate NUS
+4 -1
View File
@@ -3976,11 +3976,14 @@ class MeshCoreConnector extends ChangeNotifier {
tag: 'Connector', tag: 'Connector',
); );
// CRITICAL: Preserve user's path override when contact is refreshed from device // Preserve user-selected path settings and previously known GPS when
// refreshed frames omit coordinates (lat/lon encoded as 0,0).
_contacts[existingIndex] = contact.copyWith( _contacts[existingIndex] = contact.copyWith(
lastMessageAt: mergedLastMessageAt, lastMessageAt: mergedLastMessageAt,
pathOverride: existing.pathOverride, // Preserve user's path choice pathOverride: existing.pathOverride, // Preserve user's path choice
pathOverrideBytes: existing.pathOverrideBytes, pathOverrideBytes: existing.pathOverrideBytes,
latitude: contact.latitude ?? existing.latitude,
longitude: contact.longitude ?? existing.longitude,
); );
appLogger.info( appLogger.info(
+3
View File
@@ -7,6 +7,9 @@ class MeshCoreUuids {
"MeshCore-", "MeshCore-",
"Whisper-", "Whisper-",
"WisCore-", "WisCore-",
"Seeed",
"Lilygo",
"HT-", "HT-",
"LowMesh_MC_",
]; ];
} }
+38
View File
@@ -0,0 +1,38 @@
class GifHelper {
/// Parse a known GIF format, which can be any of:
/// g:GIFID
/// https://media.giphy.com/media/GIFID/giphy.gif
/// https://giphy.com/gifs/Optional-title-with-dashes-GIFID
///
/// GIFID is a Giphy GIF ID. The https:// is optional (and
/// can also be http://). The giphy.com/gifs form can also
/// include a trailing slash.
///
/// Returns null if text is not a valid GIF format
static String? parseGif(String text) {
final trimmed = text.trim();
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
if (match != null) {
return match.group(1);
}
final directUrlMatch = RegExp(
r'^(?:https?:\/\/)?media\.giphy\.com\/media\/([A-Za-z0-9_-]+)\/giphy\.gif$',
).firstMatch(trimmed);
if (directUrlMatch != null) {
return directUrlMatch.group(1);
}
// Giphy understands page URLs with just the ID, or any string and a
// dash before the ID, and redirects to a page with a dash-separated
// title, a dash, and the ID. IDs in this form *probably* can't
// contain dashes.
final pageMatch = RegExp(
r'^(?:https?:\/\/)?giphy\.com\/gifs\/(?:[^/?]*-)?([A-Za-z0-9_]+)\/?$',
).firstMatch(trimmed);
return pageMatch?.group(1);
}
/// Encode a GIF in a format that parseGif() can parse.
static String encodeGif(String gifId) {
return 'g:$gifId';
}
}
+5
View File
@@ -109,4 +109,9 @@ class ReactionHelper {
return ReactionInfo(targetHash: match.group(1)!, emoji: emoji); return ReactionInfo(targetHash: match.group(1)!, emoji: emoji);
} }
/// Encode a reaction message that parseReaction() can parse.
static String encodeReaction(String hash, String emojiIndex) {
return 'r:$hash:$emojiIndex';
}
} }
+1
View File
@@ -2098,6 +2098,7 @@
"translation_translateTo": "Fordítás {language}-ra", "translation_translateTo": "Fordítás {language}-ra",
"translation_translationOptions": "Fordítási lehetőségek", "translation_translationOptions": "Fordítási lehetőségek",
"translation_systemLanguage": "Rendszer nyelvé", "translation_systemLanguage": "Rendszer nyelvé",
"scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs).",
"repeater_cliQuickClockSync": "Óra szinkronizálás", "repeater_cliQuickClockSync": "Óra szinkronizálás",
"repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat" "repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat"
} }
+1 -1
View File
@@ -3677,7 +3677,7 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String scanner_linuxPairingPinPrompt(String deviceName) { String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Adja meg a PIN kódot a $deviceName számára (hagyja üresen, ha nincs).'; return 'Adja meg a(z) $deviceName PIN-kódját (hagyja üresen, ha nincs).';
} }
@override @override
+6 -11
View File
@@ -11,6 +11,7 @@ import '../connector/meshcore_connector.dart';
import '../utils/platform_info.dart'; import '../utils/platform_info.dart';
import '../helpers/chat_scroll_controller.dart'; import '../helpers/chat_scroll_controller.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../helpers/gif_helper.dart';
import '../helpers/reaction_helper.dart'; import '../helpers/reaction_helper.dart';
import '../helpers/utf8_length_limiter.dart'; import '../helpers/utf8_length_limiter.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
@@ -355,7 +356,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final settingsService = context.watch<AppSettingsService>(); final settingsService = context.watch<AppSettingsService>();
final enableTracing = settingsService.settings.enableMessageTracing; final enableTracing = settingsService.settings.enableMessageTracing;
final isOutgoing = message.isOutgoing; final isOutgoing = message.isOutgoing;
final gifId = _parseGifId(message.text); final gifId = GifHelper.parseGif(message.text);
final poi = _parsePoiMessage(message.text); final poi = _parsePoiMessage(message.text);
final translatedDisplayText = final translatedDisplayText =
message.translatedText != null && message.translatedText != null &&
@@ -699,7 +700,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7); final previewTextColor = colorScheme.onSurface.withValues(alpha: 0.7);
final gifId = _parseGifId(replyText); final gifId = GifHelper.parseGif(replyText);
final poi = _parsePoiMessage(replyText); final poi = _parsePoiMessage(replyText);
Widget contentPreview; Widget contentPreview;
@@ -811,12 +812,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
); );
} }
String? _parseGifId(String text) {
final trimmed = text.trim();
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
return match?.group(1);
}
_PoiInfo? _parsePoiMessage(String text) { _PoiInfo? _parsePoiMessage(String text) {
final trimmed = text.trim(); final trimmed = text.trim();
final match = RegExp( final match = RegExp(
@@ -897,7 +892,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
isScrollControlled: true, isScrollControlled: true,
builder: (context) => GifPicker( builder: (context) => GifPicker(
onGifSelected: (gifId) { onGifSelected: (gifId) {
_textController.text = 'g:$gifId'; _textController.text = GifHelper.encodeGif(gifId);
}, },
), ),
); );
@@ -1053,7 +1048,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
child: ValueListenableBuilder<TextEditingValue>( child: ValueListenableBuilder<TextEditingValue>(
valueListenable: _textController, valueListenable: _textController,
builder: (context, value, child) { builder: (context, value, child) {
final gifId = _parseGifId(value.text); final gifId = GifHelper.parseGif(value.text);
if (gifId != null) { if (gifId != null) {
return Focus( return Focus(
autofocus: true, autofocus: true,
@@ -1322,7 +1317,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
message.senderName, message.senderName,
message.text, message.text,
); );
final reactionText = 'r:$hash:$emojiIndex'; final reactionText = ReactionHelper.encodeReaction(hash, emojiIndex);
connector.sendChannelMessage(widget.channel, reactionText); connector.sendChannelMessage(widget.channel, reactionText);
} }
+5 -16
View File
@@ -16,6 +16,7 @@ import '../connector/meshcore_protocol.dart';
import '../helpers/reaction_helper.dart'; import '../helpers/reaction_helper.dart';
import '../widgets/message_status_icon.dart'; import '../widgets/message_status_icon.dart';
import '../helpers/chat_scroll_controller.dart'; import '../helpers/chat_scroll_controller.dart';
import '../helpers/gif_helper.dart';
import '../helpers/path_helper.dart'; import '../helpers/path_helper.dart';
import '../helpers/utf8_length_limiter.dart'; import '../helpers/utf8_length_limiter.dart';
import '../models/channel_message.dart'; import '../models/channel_message.dart';
@@ -523,7 +524,7 @@ class _ChatScreenState extends State<ChatScreen> {
child: ValueListenableBuilder<TextEditingValue>( child: ValueListenableBuilder<TextEditingValue>(
valueListenable: _textController, valueListenable: _textController,
builder: (context, value, child) { builder: (context, value, child) {
final gifId = _parseGifId(value.text); final gifId = GifHelper.parseGif(value.text);
if (gifId != null) { if (gifId != null) {
return Focus( return Focus(
autofocus: true, autofocus: true,
@@ -601,19 +602,13 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
String? _parseGifId(String text) {
final trimmed = text.trim();
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
return match?.group(1);
}
void _showGifPicker(BuildContext context) { void _showGifPicker(BuildContext context) {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: (context) => GifPicker( builder: (context) => GifPicker(
onGifSelected: (gifId) { onGifSelected: (gifId) {
_textController.text = 'g:$gifId'; _textController.text = GifHelper.encodeGif(gifId);
}, },
), ),
); );
@@ -1546,7 +1541,7 @@ class _ChatScreenState extends State<ChatScreen> {
senderName, senderName,
message.text, message.text,
); );
final reactionText = 'r:$hash:$emojiIndex'; final reactionText = ReactionHelper.encodeReaction(hash, emojiIndex);
connector.sendMessage(_resolveContact(connector), reactionText); connector.sendMessage(_resolveContact(connector), reactionText);
} }
} }
@@ -1576,7 +1571,7 @@ class _MessageBubble extends StatelessWidget {
final enableTracing = settingsService.settings.enableMessageTracing; final enableTracing = settingsService.settings.enableMessageTracing;
final isOutgoing = message.isOutgoing; final isOutgoing = message.isOutgoing;
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final gifId = _parseGifId(message.text); final gifId = GifHelper.parseGif(message.text);
final poi = _parsePoiMessage(message.text); final poi = _parsePoiMessage(message.text);
final isFailed = message.status == MessageStatus.failed; final isFailed = message.status == MessageStatus.failed;
final bubbleColor = isFailed final bubbleColor = isFailed
@@ -1850,12 +1845,6 @@ class _MessageBubble extends StatelessWidget {
); );
} }
String? _parseGifId(String text) {
final trimmed = text.trim();
final match = RegExp(r'^g:([A-Za-z0-9_-]+)$').firstMatch(trimmed);
return match?.group(1);
}
_PoiInfo? _parsePoiMessage(String text) { _PoiInfo? _parsePoiMessage(String text) {
final trimmed = text.trim(); final trimmed = text.trim();
final match = RegExp( final match = RegExp(
+5 -2
View File
@@ -6,6 +6,7 @@ import 'package:llamadart/llamadart.dart';
import '../models/app_settings.dart'; import '../models/app_settings.dart';
import '../models/translation_support.dart'; import '../models/translation_support.dart';
import '../helpers/gif_helper.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import 'app_settings_service.dart'; import 'app_settings_service.dart';
import 'translation_file_store.dart'; import 'translation_file_store.dart';
@@ -509,8 +510,10 @@ class TranslationService extends ChangeNotifier {
if (trimmed.isEmpty) { if (trimmed.isEmpty) {
return false; return false;
} }
return !(trimmed.startsWith('g:') || if (GifHelper.parseGif(trimmed) != null) {
trimmed.startsWith('m:') || return false;
}
return !(trimmed.startsWith('m:') ||
trimmed.startsWith('V1|') || trimmed.startsWith('V1|') ||
trimmed.startsWith('r:')); trimmed.startsWith('r:'));
} }
@@ -9,7 +9,6 @@ import flutter_blue_plus_darwin
import flutter_local_notifications import flutter_local_notifications
import mobile_scanner import mobile_scanner
import package_info_plus import package_info_plus
import path_provider_foundation
import share_plus import share_plus
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
@@ -20,7 +19,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))