From 596ae6c5a0300c77afc9ce1fc8788301c450ee25 Mon Sep 17 00:00:00 2001 From: Janez T Date: Sat, 18 Oct 2025 23:29:47 +0200 Subject: [PATCH] feat: Configure default public channel with well-known secret and enhance channel message handling --- CLAUDE.md | 12 ++++++++- lib/providers/app_provider.dart | 12 +++++++++ lib/providers/connection_provider.dart | 34 ++++++++++++++++++++++++ lib/services/meshcore_ble_service.dart | 21 ++++++++++++--- lib/services/meshcore_constants.dart | 10 +++++++ lib/services/protocol/frame_builder.dart | 13 ++++++++- 6 files changed, 97 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 547da50..07290f5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -528,7 +528,17 @@ Packet Structure for PAYLOAD_TYPE_GRP_TXT (0x05): - Channel 0 = "Public Channel" (default flood-mode broadcast) - Channel 1+ = Reserved for future - **Ephemeral** - messages NOT persisted -- Use `CMD_SEND_CHANNEL_TXT_MSG` +- Use `CMD_SEND_CHANNEL_TXT_MSG` (3) +- **⚠️ MUST be configured before use** with `CMD_SET_CHANNEL` (32) + - Format: `[cmd(1)][channel_idx(1)][name(32)][secret(16)]` + - **Default public channel secret (128-bit)**: + - Hex: `8b3387e9c5cdea6ac9e5edbaa115cd72` + - Base64: `izOH6cXN6mrJ5e26oRXNcg==` + - Source: [MeshCore FAQ](https://github.com/meshcore-dev/MeshCore/blob/main/docs/faq.md) + - **Configuration**: Most firmware versions have channel 0 pre-configured + - App attempts to configure via `CMD_SET_CHANNEL` during init + - If command times out, channel is likely pre-configured (this is normal) + - If not pre-configured, radio returns `ERR_CODE_NOT_FOUND` (2) on send attempts **Rooms** (ADV_TYPE_ROOM contacts): - Named contacts with public keys diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index 0f0d9e1..a78dcf4 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -262,6 +262,18 @@ class AppProvider with ChangeNotifier { await connectionProvider.syncChannels(); debugPrint('✅ [AppProvider] Channel sync complete'); + // Configure the default public channel (channel 0) + // This must be done before sending any channel messages + // Note: Some firmware versions may have this pre-configured + debugPrint('📻 [AppProvider] Configuring default public channel (channel 0)...'); + try { + await connectionProvider.configureDefaultPublicChannel(); + debugPrint('✅ [AppProvider] Public channel configured successfully'); + } catch (e) { + debugPrint('⚠️ [AppProvider] Public channel configuration failed (may already be configured): $e'); + // Continue anyway - channel might already be configured in firmware + } + // Automatically login to all saved rooms await _autoLoginToRooms(); diff --git a/lib/providers/connection_provider.dart b/lib/providers/connection_provider.dart index 8b9d057..8687b59 100644 --- a/lib/providers/connection_provider.dart +++ b/lib/providers/connection_provider.dart @@ -6,6 +6,7 @@ import '../models/contact.dart'; import '../models/message.dart'; import '../models/room_login_state.dart'; import '../services/meshcore_ble_service.dart'; +import '../services/meshcore_constants.dart'; import '../utils/sar_message_parser.dart'; import 'helpers/room_login_manager.dart'; import 'helpers/message_delivery_tracker.dart'; @@ -673,6 +674,39 @@ class ConnectionProvider with ChangeNotifier { } } + /// Configure the default public channel (channel 0) with the well-known secret + /// + /// This MUST be called after connecting to the device and before sending any + /// channel messages. Without this configuration, channel messages will fail + /// with ERR_CODE_NOT_FOUND. + /// + /// The public channel uses a well-known pre-shared key that all MeshCore + /// devices use for the default public channel. + Future configureDefaultPublicChannel() async { + if (!_bleService.isConnected) { + _error = 'Not connected to device'; + notifyListeners(); + return; + } + + try { + debugPrint('📻 [Provider] Configuring default public channel (channel 0)'); + debugPrint(' Using secret: ${MeshCoreConstants.defaultPublicChannelSecret.map((b) => b.toRadixString(16).padLeft(2, '0')).join('')}'); + await _bleService.setChannel( + channelIdx: 0, + channelName: 'Public Channel', + secret: MeshCoreConstants.defaultPublicChannelSecret, + ); + debugPrint('✅ [Provider] Public channel configured successfully'); + } catch (e) { + _error = 'Failed to configure public channel: $e'; + debugPrint('❌ [Provider] Public channel configuration failed: $e'); + debugPrint(' This may be normal if the channel is pre-configured in firmware'); + notifyListeners(); + rethrow; // Re-throw to notify caller of failure + } + } + /// Add or update a contact on the companion radio /// /// This manually adds a contact to the radio's internal contact table. diff --git a/lib/services/meshcore_ble_service.dart b/lib/services/meshcore_ble_service.dart index 9a648ca..5001337 100644 --- a/lib/services/meshcore_ble_service.dart +++ b/lib/services/meshcore_ble_service.dart @@ -315,6 +315,13 @@ class MeshCoreBleService { _responseHandler.trackSentMessage(messageId, null); } + /// Send a text message to a channel (flood-mode broadcast) + /// + /// Channel messages are ephemeral and use flood routing (no ACKs). + /// Use channel 0 for the default public channel. + /// + /// Note: Uses fire-and-forget mode since channel messages don't return + /// delivery confirmation (they're broadcast to all nodes). Future sendChannelMessage({ required int channelIdx, required String text, @@ -324,6 +331,8 @@ class MeshCoreBleService { throw ArgumentError('Channel message too long (max ~160 characters)'); } + // Channel messages use fire-and-forget (no ACK expected) + // The firmware responds with RESP_CODE_OK but we don't wait for it await _commandSender.writeData(FrameBuilder.buildSendChannelTxtMsg( channelIdx: channelIdx, text: text, @@ -483,20 +492,26 @@ class MeshCoreBleService { await _commandSender.writeData(FrameBuilder.buildGetChannel(channelIdx)); } - /// Set the name for a specific channel + /// Set the name and secret for a specific channel + /// + /// The secret must be exactly 16 bytes (128-bit encryption key). + /// For the default public channel (channel 0), use [MeshCoreConstants.defaultPublicChannelSecret]. Future setChannel({ required int channelIdx, required String channelName, + required List secret, }) async { - debugPrint('📻 [BLE] Setting channel name:'); + debugPrint('📻 [BLE] Setting channel:'); debugPrint(' Channel index: $channelIdx'); debugPrint(' Channel name: $channelName'); + debugPrint(' Secret length: ${secret.length} bytes'); await _commandSender.writeDataAndWaitForAck(FrameBuilder.buildSetChannel( channelIdx: channelIdx, channelName: channelName, + secret: secret, )); - debugPrint('✅ [BLE] CMD_SET_CHANNEL sent'); + debugPrint('✅ [BLE] CMD_SET_CHANNEL sent successfully'); } /// Sync all channels from the device (typically 0-39) diff --git a/lib/services/meshcore_constants.dart b/lib/services/meshcore_constants.dart index 552452d..36cee01 100644 --- a/lib/services/meshcore_constants.dart +++ b/lib/services/meshcore_constants.dart @@ -117,6 +117,16 @@ class MeshCoreConstants { static const int binaryReqGetAccessList = 0x05; static const int binaryReqGetNeighbours = 0x06; + // Default Public Channel Secret (128-bit) + // This is the well-known pre-shared key for the public channel (channel 0) + // Hex: 8b3387e9c5cdea6ac9e5edbaa115cd72 + // Base64: izOH6cXN6mrJ5e26oRXNcg== + // Source: https://github.com/meshcore-dev/MeshCore/blob/main/docs/faq.md + static const List defaultPublicChannelSecret = [ + 0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a, + 0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72, + ]; + // Cayenne LPP Data Types static const int lppDigitalInput = 0; static const int lppDigitalOutput = 1; diff --git a/lib/services/protocol/frame_builder.dart b/lib/services/protocol/frame_builder.dart index 30ea2dc..7ae669a 100644 --- a/lib/services/protocol/frame_builder.dart +++ b/lib/services/protocol/frame_builder.dart @@ -252,11 +252,19 @@ class FrameBuilder { return writer.toBytes(); } - /// Build SetChannel command - sets the name for a specific channel + /// Build SetChannel command - sets the name and secret for a specific channel + /// + /// Format: [cmd(1)][channel_idx(1)][name(32)][secret(16)] + /// Secret must be exactly 16 bytes (128-bit key) static Uint8List buildSetChannel({ required int channelIdx, required String channelName, + required List secret, }) { + if (secret.length != 16) { + throw ArgumentError('Channel secret must be exactly 16 bytes (got ${secret.length})'); + } + final writer = BufferWriter(); writer.writeByte(MeshCoreConstants.cmdSetChannel); // 0x20 (32) writer.writeByte(channelIdx); // 0-39 typically @@ -268,6 +276,9 @@ class FrameBuilder { nameBytes.setRange(0, copyLen, encoded); writer.writeBytes(nameBytes); + // Write 16-byte secret + writer.writeBytes(Uint8List.fromList(secret)); + return writer.toBytes(); } }