mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
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:
269
lib/services/ble/ble_command_queue.dart
Normal file
269
lib/services/ble/ble_command_queue.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user