feat: Implement command queue for BLE command handling

- Added BleCommandQueue to manage command serialization and responses in BleCommandSender.
- Updated writeData, writeDataAndWaitForAck, and writeDataAndWaitForResponse methods to utilize the command queue.
- Enhanced BleResponseHandler to complete commands based on responses received from the BLE device.
- Introduced new commands for channel management, including getChannel and setChannel.
- Created LocationTrailLayer and TrailControls widgets for displaying and managing location trails on the map.
- Added PermissionRequestDialog to handle location permission requests on app startup.
- Updated LocationTrackingService to allow GPS tracking without a BLE connection.
This commit is contained in:
Janez T
2025-10-18 21:12:02 +02:00
parent f88c9cfdd3
commit c50a260263
35 changed files with 1854 additions and 59 deletions

View File

@@ -0,0 +1,269 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
/// Type of response expected from a command
enum CommandResponseType {
/// No response expected (fire-and-forget)
none,
/// Wait for RESP_CODE_OK (0) or RESP_CODE_ERR (1)
ack,
/// Wait for specific response code with data
data,
}
/// Represents a queued BLE command
class QueuedCommand<T> {
/// The command data to send
final Uint8List data;
/// Command code (first byte of data)
final int commandCode;
/// Type of response expected
final CommandResponseType responseType;
/// Expected response code (for data type commands)
final int? expectedResponseCode;
/// Completer to signal command completion
final Completer<T> completer;
/// Timeout duration for this command
final Duration timeout;
/// Timestamp when command was enqueued
final DateTime enqueuedAt;
QueuedCommand({
required this.data,
required this.commandCode,
required this.responseType,
this.expectedResponseCode,
required this.completer,
required this.timeout,
}) : enqueuedAt = DateTime.now();
}
/// BLE command queue with mutex lock and inter-command delays
///
/// Ensures that:
/// - Only one command executes at a time
/// - 100ms delay between all commands
/// - Commands can wait for ACK or specific responses
/// - Timeouts are enforced
class BleCommandQueue {
// Queue of pending commands
final List<QueuedCommand> _queue = [];
// Mutex lock using Completer
Completer<void> _lock = Completer<void>()..complete();
// Whether queue is currently processing
bool _isProcessing = false;
// Pending responses mapped by command code
final Map<int, QueuedCommand> _pendingResponses = {};
// Last command execution timestamp
DateTime? _lastCommandTime;
// Minimum delay between commands (milliseconds)
static const int _minDelayMs = 100;
// Callbacks
VoidCallback? onQueueEmpty;
void Function(int queueSize)? onQueueSizeChanged;
/// Enqueue a command and wait for it to complete
///
/// [data] - The command data to send
/// [commandCode] - Command code (first byte)
/// [responseType] - Type of response expected
/// [expectedResponseCode] - For data responses, the expected response code
/// [timeout] - Maximum time to wait for response
///
/// Returns a Future that completes when the command receives its response
/// or throws TimeoutException if timeout expires.
Future<T> enqueue<T>({
required Uint8List data,
required int commandCode,
required CommandResponseType responseType,
int? expectedResponseCode,
Duration? timeout,
}) async {
// Determine timeout based on response type
final cmdTimeout = timeout ??
(responseType == CommandResponseType.data
? const Duration(seconds: 10)
: const Duration(seconds: 5));
// Create queued command
final command = QueuedCommand<T>(
data: data,
commandCode: commandCode,
responseType: responseType,
expectedResponseCode: expectedResponseCode,
completer: Completer<T>(),
timeout: cmdTimeout,
);
// Add to queue
_queue.add(command);
onQueueSizeChanged?.call(_queue.length);
debugPrint('📋 [CommandQueue] Enqueued command 0x${commandCode.toRadixString(16).padLeft(2, '0')} (queue size: ${_queue.length})');
// Start processing if not already running
if (!_isProcessing) {
_processQueue();
}
// Wait for command to complete or timeout
return command.completer.future.timeout(
cmdTimeout,
onTimeout: () {
debugPrint('⏱️ [CommandQueue] Command 0x${commandCode.toRadixString(16).padLeft(2, '0')} timed out after ${cmdTimeout.inSeconds}s');
_pendingResponses.remove(commandCode);
throw TimeoutException('Command 0x${commandCode.toRadixString(16).padLeft(2, '0')} timed out');
},
);
}
/// Process the command queue
Future<void> _processQueue() async {
if (_isProcessing) return;
_isProcessing = true;
while (_queue.isNotEmpty) {
// Wait for lock
await _lock.future;
// Get next command
final command = _queue.removeAt(0);
onQueueSizeChanged?.call(_queue.length);
try {
// Enforce minimum delay between commands
if (_lastCommandTime != null) {
final elapsed = DateTime.now().difference(_lastCommandTime!);
final remainingDelay = _minDelayMs - elapsed.inMilliseconds;
if (remainingDelay > 0) {
debugPrint('⏸️ [CommandQueue] Waiting ${remainingDelay}ms before next command');
await Future.delayed(Duration(milliseconds: remainingDelay));
}
}
// Create new lock for next command
_lock = Completer<void>();
// Register for response if needed
if (command.responseType != CommandResponseType.none) {
final responseKey = command.responseType == CommandResponseType.ack
? command.commandCode
: (command.expectedResponseCode ?? command.commandCode);
_pendingResponses[responseKey] = command;
}
// Execute command (handled by BleCommandSender)
// The completer will be completed by completeCommand() when response arrives
debugPrint('📤 [CommandQueue] Executing command 0x${command.commandCode.toRadixString(16).padLeft(2, '0')}');
// For fire-and-forget commands, complete immediately
if (command.responseType == CommandResponseType.none) {
command.completer.complete(null);
}
// Update last command time
_lastCommandTime = DateTime.now();
// Release lock after minimum delay
Future.delayed(const Duration(milliseconds: _minDelayMs), () {
if (!_lock.isCompleted) {
_lock.complete();
}
});
} catch (e) {
debugPrint('❌ [CommandQueue] Error processing command: $e');
if (!command.completer.isCompleted) {
command.completer.completeError(e);
}
// Release lock on error
if (!_lock.isCompleted) {
_lock.complete();
}
}
}
_isProcessing = false;
onQueueEmpty?.call();
debugPrint('✅ [CommandQueue] Queue empty');
}
/// Complete a pending command with response data
///
/// Called by BleResponseHandler when a response is received
void completeCommand<T>(int responseCode, T data) {
final command = _pendingResponses.remove(responseCode);
if (command != null) {
debugPrint('✅ [CommandQueue] Completing command 0x${command.commandCode.toRadixString(16).padLeft(2, '0')} with response 0x${responseCode.toRadixString(16).padLeft(2, '0')}');
if (!command.completer.isCompleted) {
command.completer.complete(data);
}
}
}
/// Complete a pending command with error
///
/// Called by BleResponseHandler when RESP_CODE_ERR is received
void completeCommandWithError(int commandCode, String error, {int? errorCode}) {
final command = _pendingResponses.remove(commandCode);
if (command != null) {
debugPrint('❌ [CommandQueue] Command 0x${commandCode.toRadixString(16).padLeft(2, '0')} failed: $error (code: $errorCode)');
if (!command.completer.isCompleted) {
command.completer.completeError(
Exception('Command failed: $error (error code: $errorCode)'),
);
}
}
}
/// Get current queue size
int get queueSize => _queue.length;
/// Get number of pending responses
int get pendingResponseCount => _pendingResponses.length;
/// Check if queue is empty
bool get isEmpty => _queue.isEmpty;
/// Check if queue is processing
bool get isProcessing => _isProcessing;
/// Clear all pending commands (use with caution)
void clear() {
debugPrint('🗑️ [CommandQueue] Clearing queue (${_queue.length} commands, ${_pendingResponses.length} pending responses)');
// Complete all pending commands with error
for (final command in _pendingResponses.values) {
if (!command.completer.isCompleted) {
command.completer.completeError(Exception('Queue cleared'));
}
}
_queue.clear();
_pendingResponses.clear();
onQueueSizeChanged?.call(0);
}
/// Dispose resources
void dispose() {
clear();
if (!_lock.isCompleted) {
_lock.complete();
}
}
}

View File

@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import '../meshcore_opcode_names.dart';
import '../../models/ble_packet_log.dart';
import 'ble_command_queue.dart';
/// Callback types for sender events
typedef OnErrorCallback = void Function(String error);
@@ -14,6 +15,9 @@ class BleCommandSender {
final List<BlePacketLog> _packetLogs = [];
static const int _maxLogSize = 1000;
// Command queue for serialization and response waiting
final BleCommandQueue _commandQueue = BleCommandQueue();
// Callbacks
OnErrorCallback? onError;
VoidCallback? onTxActivity;
@@ -21,17 +25,99 @@ class BleCommandSender {
// Getters
int get txPacketCount => _txPacketCount;
List<BlePacketLog> get packetLogs => List.unmodifiable(_packetLogs);
BleCommandQueue get commandQueue => _commandQueue;
/// Set the RX characteristic to write to
void setRxCharacteristic(BluetoothCharacteristic? characteristic) {
_rxCharacteristic = characteristic;
}
/// Write data to RX characteristic
/// Write data to RX characteristic (fire-and-forget, no response expected)
///
/// This method is for commands that don't expect any response.
/// The command is queued and executed with proper spacing, but we don't wait
/// for any acknowledgment.
Future<void> writeData(Uint8List data) async {
if (_rxCharacteristic == null) {
throw Exception('Not connected');
}
final commandCode = data.isNotEmpty ? data[0] : 0;
// Enqueue the command (fire-and-forget)
await _commandQueue.enqueue<void>(
data: data,
commandCode: commandCode,
responseType: CommandResponseType.none,
);
// Actually send the data
await _sendToDevice(data);
}
/// Write data and wait for ACK (RESP_CODE_OK or RESP_CODE_ERR)
///
/// This method should be used for setup commands that return RESP_CODE_OK (0)
/// on success or RESP_CODE_ERR (1) on failure.
///
/// Examples: setAdvertLatLon, setAdvertName, setRadioParams, etc.
Future<void> writeDataAndWaitForAck(Uint8List data) async {
if (_rxCharacteristic == null) {
throw Exception('Not connected');
}
final commandCode = data.isNotEmpty ? data[0] : 0;
// Enqueue the command (wait for ACK)
await _commandQueue.enqueue<void>(
data: data,
commandCode: commandCode,
responseType: CommandResponseType.ack,
);
// Actually send the data
await _sendToDevice(data);
}
/// Write data and wait for specific response
///
/// This method should be used for query commands that return specific data.
///
/// Examples:
/// - CMD_DEVICE_QUERY → RESP_CODE_DEVICE_INFO
/// - CMD_APP_START → RESP_CODE_SELF_INFO
/// - CMD_GET_CONTACTS → RESP_CODE_CONTACTS_START
Future<T> writeDataAndWaitForResponse<T>(
Uint8List data,
int expectedResponseCode,
) async {
if (_rxCharacteristic == null) {
throw Exception('Not connected');
}
final commandCode = data.isNotEmpty ? data[0] : 0;
// Enqueue the command (wait for specific response)
final responseFuture = _commandQueue.enqueue<T>(
data: data,
commandCode: commandCode,
responseType: CommandResponseType.data,
expectedResponseCode: expectedResponseCode,
);
// Actually send the data
await _sendToDevice(data);
// Wait for response
return responseFuture;
}
/// Internal method to actually send data to the BLE device
Future<void> _sendToDevice(Uint8List data) async {
if (_rxCharacteristic == null) {
throw Exception('Not connected');
}
try {
// Extract command code from first byte
final commandCode = data.isNotEmpty ? data[0] : null;
@@ -125,6 +211,7 @@ class BleCommandSender {
/// Dispose resources
void dispose() {
_commandQueue.dispose();
_rxCharacteristic = null;
_packetLogs.clear();
}

View File

@@ -10,6 +10,7 @@ import '../buffer_reader.dart';
import '../meshcore_constants.dart';
import '../meshcore_opcode_names.dart';
import '../protocol/frame_parser.dart';
import 'ble_command_queue.dart';
/// Callback types for response events
typedef OnContactCallback = void Function(Contact contact);
@@ -31,6 +32,7 @@ typedef OnBinaryResponseCallback = void Function(Uint8List publicKeyPrefix, int
typedef OnBatteryAndStorageCallback = void Function(int millivolts, int? usedKb, int? totalKb);
typedef OnErrorCallback = void Function(String error, {int? errorCode});
typedef OnContactNotFoundCallback = void Function(Uint8List? contactPublicKey);
typedef OnChannelInfoCallback = void Function(int channelIdx, String channelName);
/// Processes incoming responses from the BLE device
class BleResponseHandler {
@@ -40,6 +42,9 @@ class BleResponseHandler {
final List<BlePacketLog> _packetLogs = [];
static const int _maxLogSize = 1000;
// Reference to command queue for completing pending commands
BleCommandQueue? _commandQueue;
// Callbacks
OnContactCallback? onContactReceived;
OnContactsCompleteCallback? onContactsComplete;
@@ -60,6 +65,7 @@ class BleResponseHandler {
OnBatteryAndStorageCallback? onBatteryAndStorage;
OnErrorCallback? onError;
OnContactNotFoundCallback? onContactNotFound;
OnChannelInfoCallback? onChannelInfoReceived;
VoidCallback? onRxActivity;
// Track the last command that was sent, so we can retry if it fails with ERR_CODE_NOT_FOUND
@@ -69,6 +75,11 @@ class BleResponseHandler {
int get rxPacketCount => _rxPacketCount;
List<BlePacketLog> get packetLogs => List.unmodifiable(_packetLogs);
/// Set the command queue for completing pending commands
void setCommandQueue(BleCommandQueue? queue) {
_commandQueue = queue;
}
/// Subscribe to TX characteristic notifications
void subscribeToNotifications(BluetoothCharacteristic txCharacteristic) {
_txSubscription = txCharacteristic.lastValueStream.listen(
@@ -195,12 +206,18 @@ class BleResponseHandler {
print(' → Handling BatteryAndStorage');
_handleBatteryAndStorage(reader);
break;
case MeshCoreConstants.respChannelInfo:
print(' → Handling ChannelInfo');
_handleChannelInfo(reader);
break;
case MeshCoreConstants.respNoMoreMessages:
print(' → Response: No More Messages');
onNoMoreMessages?.call();
break;
case MeshCoreConstants.respOk:
print(' → Response: OK');
// Complete any pending ACK command
_commandQueue?.completeCommand<void>(MeshCoreConstants.respOk, null);
break;
case MeshCoreConstants.respErr:
print(' → Response: ERROR');
@@ -249,6 +266,13 @@ class BleResponseHandler {
final result = FrameParser.parseSentConfirmation(reader);
if (result.isNotEmpty) {
print(' ✅ [Sent] Message sent successfully');
// Complete any pending command waiting for sent confirmation
_commandQueue?.completeCommand<Map<String, dynamic>>(
MeshCoreConstants.respSent,
result,
);
onMessageSent?.call(
result['expectedAckTag'] as int,
result['suggestedTimeout'] as int,
@@ -319,6 +343,13 @@ class BleResponseHandler {
void _handleDeviceInfo(BufferReader reader) {
try {
final info = FrameParser.parseDeviceInfo(reader);
// Complete any pending command waiting for device info
_commandQueue?.completeCommand<Map<String, dynamic>>(
MeshCoreConstants.respDeviceInfo,
info,
);
onDeviceInfoReceived?.call(info);
print(' ✅ [DeviceInfo] Parsed successfully');
} catch (e) {
@@ -331,9 +362,16 @@ class BleResponseHandler {
void _handleSelfInfo(BufferReader reader) {
try {
final info = FrameParser.parseSelfInfo(reader);
// Complete any pending command waiting for self info
if (info.isNotEmpty) {
_commandQueue?.completeCommand<Map<String, dynamic>>(
MeshCoreConstants.respSelfInfo,
info,
);
onSelfInfoReceived?.call(info);
}
print(' ✅ [SelfInfo] Parsed successfully');
} catch (e) {
print(' ❌ [SelfInfo] Parsing error: $e');
@@ -572,6 +610,23 @@ class BleResponseHandler {
}
}
/// Handle ChannelInfo response
void _handleChannelInfo(BufferReader reader) {
try {
final info = FrameParser.parseChannelInfo(reader);
if (info.isNotEmpty) {
final channelIdx = info['channelIdx'] as int;
final channelName = info['channelName'] as String;
print(' ✅ [ChannelInfo] Channel $channelIdx: "${channelName}"');
onChannelInfoReceived?.call(channelIdx, channelName);
}
} catch (e) {
print(' ❌ [ChannelInfo] Parsing error: $e');
onError?.call('ChannelInfo parsing error: $e');
}
}
/// Handle Error response
void _handleError(BufferReader reader) {
try {
@@ -580,6 +635,13 @@ class BleResponseHandler {
final errorMsg = FrameParser.getErrorMessage(errorCode);
print(' ❌ [Error] $errorMsg');
// Complete any pending ACK command with error
_commandQueue?.completeCommandWithError(
MeshCoreConstants.respOk, // Command was expecting OK, got ERR
errorMsg,
errorCode: errorCode,
);
// Special handling for ERR_CODE_NOT_FOUND (2) - contact not in radio
if (errorCode == 2) { // ERR_CODE_NOT_FOUND
print(' ⚠️ [Error] Contact not found in radio - attempting auto-recovery');

View File

@@ -246,19 +246,20 @@ class LocationTrackingService {
/// Returns true if successful, false otherwise.
/// Note: This method returns immediately after starting the position stream.
/// Initial position acquisition happens asynchronously in the background.
///
/// GPS tracking works WITHOUT BLE connection - device broadcasts are simply skipped.
Future<bool> startTracking({double? distanceThreshold}) async {
if (!_isInitialized || _bleService == null) {
if (!_isInitialized) {
debugPrint(
'⚠️ [LocationTracking] Service not initialized or BLE service null',
'⚠️ [LocationTracking] Service not initialized',
);
onError?.call('Location tracking service not initialized');
return false;
}
if (!_bleService!.isConnected) {
debugPrint('⚠️ [LocationTracking] BLE not connected');
onError?.call('Not connected to mesh device');
return false;
// Allow tracking without BLE connection - broadcasts will be skipped
if (_bleService == null || !_bleService!.isConnected) {
debugPrint(' [LocationTracking] Starting GPS tracking without BLE connection (broadcasts disabled)');
}
// Check permissions

View File

@@ -9,6 +9,7 @@ import 'ble/ble_connection_manager.dart';
import 'ble/ble_command_sender.dart';
import 'ble/ble_response_handler.dart';
import 'protocol/frame_builder.dart';
import 'meshcore_constants.dart';
/// Callback types for MeshCore events
typedef OnContactCallback = void Function(Contact contact);
@@ -30,6 +31,7 @@ typedef OnBinaryResponseCallback = void Function(Uint8List publicKeyPrefix, int
typedef OnBatteryAndStorageCallback = void Function(int millivolts, int? usedKb, int? totalKb);
typedef OnErrorCallback = void Function(String error, {int? errorCode});
typedef OnContactNotFoundCallback = void Function(Uint8List? contactPublicKey);
typedef OnChannelInfoCallback = void Function(int channelIdx, String channelName);
typedef OnConnectionStateCallback = void Function(bool isConnected);
typedef OnReconnectionAttemptCallback = void Function(int attemptNumber, int maxAttempts);
typedef OnRssiUpdateCallback = void Function(int rssi);
@@ -64,6 +66,7 @@ class MeshCoreBleService {
OnBatteryAndStorageCallback? onBatteryAndStorage;
OnErrorCallback? onError;
OnContactNotFoundCallback? onContactNotFound;
OnChannelInfoCallback? onChannelInfoReceived;
// Activity callbacks (for blinking indicators)
VoidCallback? onRxActivity;
@@ -157,6 +160,9 @@ class MeshCoreBleService {
_responseHandler.onContactNotFound = (contactPublicKey) {
onContactNotFound?.call(contactPublicKey);
};
_responseHandler.onChannelInfoReceived = (channelIdx, channelName) {
onChannelInfoReceived?.call(channelIdx, channelName);
};
_responseHandler.onRxActivity = () {
onRxActivity?.call();
};
@@ -185,16 +191,30 @@ class MeshCoreBleService {
Future<bool> connect(BluetoothDevice device) async {
final success = await _connectionManager.connect(device);
if (success) {
// Setup command sender with RX characteristic
_commandSender.setRxCharacteristic(_connectionManager.rxCharacteristic);
try {
// Setup command sender with RX characteristic
_commandSender.setRxCharacteristic(_connectionManager.rxCharacteristic);
// Setup response handler with TX characteristic
if (_connectionManager.txCharacteristic != null) {
_responseHandler.subscribeToNotifications(_connectionManager.txCharacteristic!);
// Wire up command queue between sender and response handler
_responseHandler.setCommandQueue(_commandSender.commandQueue);
// Setup response handler with TX characteristic
if (_connectionManager.txCharacteristic != null) {
_responseHandler.subscribeToNotifications(_connectionManager.txCharacteristic!);
}
// Send initial device query and wait for responses
await _sendDeviceQuery();
print('✅ [Service] Device initialization complete');
return true;
} catch (e) {
print('❌ [Service] Device initialization failed: $e');
// Disconnect on initialization failure
await disconnect();
onError?.call('Device initialization failed: $e');
return false;
}
// Send initial device query
await _sendDeviceQuery();
}
return success;
}
@@ -206,17 +226,30 @@ class MeshCoreBleService {
/// Send initial device query and sync clock
Future<void> _sendDeviceQuery() async {
// CRITICAL: Set device clock FIRST, before any other commands
// This ensures the device has correct timestamps for all operations
print(' [Service] Setting device clock before device query');
// STEP 1: Send device query FIRST to get device capabilities
// This is the first command to send per protocol documentation
print('🔍 [Service] Querying device information (CMD_DEVICE_QUERY)...');
final deviceInfo = await _commandSender.writeDataAndWaitForResponse<Map<String, dynamic>>(
FrameBuilder.buildDeviceQuery(),
MeshCoreConstants.respDeviceInfo,
);
print('✅ [Service] Device info received: firmware=${deviceInfo['firmwareVersion']}');
// STEP 2: Send app start to initialize the app session
// This is the first command after connection per protocol documentation
print('🚀 [Service] Sending app start (CMD_APP_START)...');
final selfInfo = await _commandSender.writeDataAndWaitForResponse<Map<String, dynamic>>(
FrameBuilder.buildAppStart(),
MeshCoreConstants.respSelfInfo,
);
print('✅ [Service] Self info received: node initialized');
// STEP 3: Set device clock AFTER initialization
// This ensures the device has correct timestamps for all subsequent operations
// Note: This command does not return an ACK, so we use writeData (fire-and-forget)
print('⏰ [Service] Setting device clock (CMD_SET_DEVICE_TIME)...');
await _commandSender.writeData(FrameBuilder.buildSetDeviceTime());
// Small delay to ensure clock is set before proceeding
await Future.delayed(const Duration(milliseconds: 100));
// Now send device query and app start
await _commandSender.writeData(FrameBuilder.buildDeviceQuery());
await _commandSender.writeData(FrameBuilder.buildAppStart());
print('✅ [Service] Device clock sent (no ACK expected)');
}
/// Refresh device info (public method)
@@ -333,7 +366,7 @@ class MeshCoreBleService {
/// Set advertised name
Future<void> setAdvertName(String name) async {
await _commandSender.writeData(FrameBuilder.buildSetAdvertName(name));
await _commandSender.writeDataAndWaitForAck(FrameBuilder.buildSetAdvertName(name));
}
/// Set advertised latitude and longitude
@@ -341,7 +374,7 @@ class MeshCoreBleService {
required double latitude,
required double longitude,
}) async {
await _commandSender.writeData(FrameBuilder.buildSetAdvertLatLon(
await _commandSender.writeDataAndWaitForAck(FrameBuilder.buildSetAdvertLatLon(
latitude: latitude,
longitude: longitude,
));
@@ -354,7 +387,7 @@ class MeshCoreBleService {
required int spreadingFactor,
required int codingRate,
}) async {
await _commandSender.writeData(FrameBuilder.buildSetRadioParams(
await _commandSender.writeDataAndWaitForAck(FrameBuilder.buildSetRadioParams(
frequency: frequency,
bandwidth: bandwidth,
spreadingFactor: spreadingFactor,
@@ -364,7 +397,7 @@ class MeshCoreBleService {
/// Set transmit power
Future<void> setTxPower(int powerDbm) async {
await _commandSender.writeData(FrameBuilder.buildSetTxPower(powerDbm));
await _commandSender.writeDataAndWaitForAck(FrameBuilder.buildSetTxPower(powerDbm));
}
/// Set other parameters
@@ -374,7 +407,7 @@ class MeshCoreBleService {
required int advertLocationPolicy,
int multiAcks = 0,
}) async {
await _commandSender.writeData(FrameBuilder.buildSetOtherParams(
await _commandSender.writeDataAndWaitForAck(FrameBuilder.buildSetOtherParams(
manualAddContacts: manualAddContacts,
telemetryModes: telemetryModes,
advertLocationPolicy: advertLocationPolicy,
@@ -426,6 +459,41 @@ class MeshCoreBleService {
print('✅ [BLE] CMD_REMOVE_CONTACT sent');
}
/// Get information for a specific channel
Future<void> getChannel(int channelIdx) async {
await _commandSender.writeData(FrameBuilder.buildGetChannel(channelIdx));
}
/// Set the name for a specific channel
Future<void> setChannel({
required int channelIdx,
required String channelName,
}) async {
print('📻 [BLE] Setting channel name:');
print(' Channel index: $channelIdx');
print(' Channel name: $channelName');
await _commandSender.writeDataAndWaitForAck(FrameBuilder.buildSetChannel(
channelIdx: channelIdx,
channelName: channelName,
));
print('✅ [BLE] CMD_SET_CHANNEL sent');
}
/// Sync all channels from the device (typically 0-39)
/// This queries each channel to get its name and metadata
Future<void> syncAllChannels({int maxChannels = 40}) async {
print('📻 [Service] Syncing channels (0-${maxChannels - 1})...');
for (int i = 0; i < maxChannels; i++) {
await getChannel(i);
// Small delay to avoid overwhelming the device
await Future.delayed(const Duration(milliseconds: 50));
}
print('✅ [Service] Channel sync complete');
}
/// Clear packet logs
void clearPacketLogs() {
_commandSender.clearPacketLogs();

View File

@@ -243,4 +243,31 @@ class FrameBuilder {
writer.writeBytes(contactPublicKey); // 32 bytes
return writer.toBytes();
}
/// Build GetChannel command - retrieves information for a specific channel
static Uint8List buildGetChannel(int channelIdx) {
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdGetChannel); // 0x1F (31)
writer.writeByte(channelIdx); // 0-39 typically
return writer.toBytes();
}
/// Build SetChannel command - sets the name for a specific channel
static Uint8List buildSetChannel({
required int channelIdx,
required String channelName,
}) {
final writer = BufferWriter();
writer.writeByte(MeshCoreConstants.cmdSetChannel); // 0x20 (32)
writer.writeByte(channelIdx); // 0-39 typically
// Write channel name as null-terminated string in 32-byte field
final nameBytes = Uint8List(32);
final encoded = utf8.encode(channelName);
final copyLen = encoded.length > 31 ? 31 : encoded.length;
nameBytes.setRange(0, copyLen, encoded);
writer.writeBytes(nameBytes);
return writer.toBytes();
}
}

View File

@@ -387,6 +387,28 @@ class FrameParser {
return null;
}
/// Parse ChannelInfo response
static Map<String, dynamic> parseChannelInfo(BufferReader reader) {
if (reader.remainingBytesCount < 33) {
return {};
}
final channelIdx = reader.readByte();
final channelName = reader.readCString(32);
// Additional fields if present in protocol
int? flags;
if (reader.hasRemaining) {
flags = reader.readByte();
}
return {
'channelIdx': channelIdx,
'channelName': channelName,
'flags': flags,
};
}
/// Get error message from error code
static String getErrorMessage(int errorCode) {
switch (errorCode) {