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

75
lib/models/channel.dart Normal file
View File

@@ -0,0 +1,75 @@
/// Channel model - represents a communication channel
class Channel {
final int index;
final String name;
final int? flags;
Channel({
required this.index,
required this.name,
this.flags,
});
/// Display name for the channel
/// Returns "Public" for channel 0, otherwise returns the custom name or "Channel N"
String get displayName {
if (index == 0) {
return name.isEmpty ? 'Public' : name;
}
return name.isEmpty ? 'Channel $index' : name;
}
/// Check if channel is the public channel (index 0)
bool get isPublicChannel => index == 0;
/// Check if channel has a custom name
bool get hasCustomName => name.isNotEmpty;
/// Create from JSON
factory Channel.fromJson(Map<String, dynamic> json) {
return Channel(
index: json['index'] as int,
name: json['name'] as String? ?? '',
flags: json['flags'] as int?,
);
}
/// Convert to JSON
Map<String, dynamic> toJson() {
return {
'index': index,
'name': name,
'flags': flags,
};
}
/// Create a copy with modified fields
Channel copyWith({
int? index,
String? name,
int? flags,
}) {
return Channel(
index: index ?? this.index,
name: name ?? this.name,
flags: flags ?? this.flags,
);
}
@override
String toString() {
return 'Channel(index: $index, name: $name, flags: $flags)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Channel &&
other.index == index &&
other.name == name &&
other.flags == flags;
}
@override
int get hashCode => Object.hash(index, name, flags);
}

View File

@@ -0,0 +1,106 @@
import 'package:latlong2/latlong.dart';
/// Represents a single point in a location trail
class TrailPoint {
final LatLng position;
final DateTime timestamp;
final double? accuracy;
final double? speed;
TrailPoint({
required this.position,
required this.timestamp,
this.accuracy,
this.speed,
});
Map<String, dynamic> toJson() => {
'lat': position.latitude,
'lon': position.longitude,
'timestamp': timestamp.toIso8601String(),
'accuracy': accuracy,
'speed': speed,
};
factory TrailPoint.fromJson(Map<String, dynamic> json) {
return TrailPoint(
position: LatLng(json['lat'] as double, json['lon'] as double),
timestamp: DateTime.parse(json['timestamp'] as String),
accuracy: json['accuracy'] as double?,
speed: json['speed'] as double?,
);
}
}
/// Represents a location trail (breadcrumb trail) on the map
class LocationTrail {
final String id;
final List<TrailPoint> points;
final DateTime startTime;
DateTime? endTime;
bool isActive;
LocationTrail({
required this.id,
List<TrailPoint>? points,
DateTime? startTime,
this.endTime,
this.isActive = true,
}) : points = points ?? [],
startTime = startTime ?? DateTime.now();
/// Add a new point to the trail
void addPoint(TrailPoint point) {
points.add(point);
}
/// Get total distance traveled in meters
double get totalDistance {
if (points.length < 2) return 0;
final distance = Distance();
double total = 0;
for (int i = 0; i < points.length - 1; i++) {
total += distance.as(
LengthUnit.Meter,
points[i].position,
points[i + 1].position,
);
}
return total;
}
/// Get duration of the trail
Duration get duration {
if (points.isEmpty) return Duration.zero;
final end = endTime ?? DateTime.now();
return end.difference(startTime);
}
/// Get list of LatLng points for rendering
List<LatLng> get latLngPoints => points.map((p) => p.position).toList();
Map<String, dynamic> toJson() => {
'id': id,
'points': points.map((p) => p.toJson()).toList(),
'startTime': startTime.toIso8601String(),
'endTime': endTime?.toIso8601String(),
'isActive': isActive,
};
factory LocationTrail.fromJson(Map<String, dynamic> json) {
return LocationTrail(
id: json['id'] as String,
points: (json['points'] as List)
.map((p) => TrailPoint.fromJson(p as Map<String, dynamic>))
.toList(),
startTime: DateTime.parse(json['startTime'] as String),
endTime: json['endTime'] != null
? DateTime.parse(json['endTime'] as String)
: null,
isActive: json['isActive'] as bool? ?? true,
);
}
}