145 lines
4.6 KiB
Dart
145 lines
4.6 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:intaleq_maps/intaleq_maps.dart';
|
|
|
|
import '../models/friendly_unit.dart';
|
|
|
|
/// ============================================================================
|
|
/// [LocalNetworkTrackerService] - محرك التتبع المحلي للوحدات (Blue Force Tracking)
|
|
/// ============================================================================
|
|
/// English:
|
|
/// Uses UDP Broadcast (Port 4545) to transmit and receive live GPS coordinates
|
|
/// of friendly forces over a localized tactical MANET or Wi-Fi mesh network,
|
|
/// entirely offline with zero dependency on cellular data.
|
|
///
|
|
/// العربية:
|
|
/// محرك التتبع المحلي عبر شبكات الراديو أو الواي فاي (Mesh/MANET).
|
|
/// يستخدم بروتوكول UDP Broadcast لإرسال واستقبال إحداثيات القوات الصديقة
|
|
/// وعرضها على خريطة القائد ميدانياً بدون إنترنت.
|
|
/// ============================================================================
|
|
class LocalNetworkTrackerService extends GetxService {
|
|
static const int broadcastPort = 4545;
|
|
|
|
// My Device Info
|
|
final String myDeviceId = 'DEV_${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}';
|
|
final String myCallsign = 'Alpha-1 (Commander)';
|
|
final UnitRole myRole = UnitRole.commander;
|
|
|
|
// Rx Map of all active friendly units nearby
|
|
final RxMap<String, FriendlyUnit> friendlyUnits = <String, FriendlyUnit>{}.obs;
|
|
|
|
RawDatagramSocket? _socket;
|
|
Timer? _broadcastTimer;
|
|
Timer? _cleanupTimer;
|
|
|
|
// The last known GPS position of this device
|
|
LatLng? _currentPosition;
|
|
double _currentHeading = 0.0;
|
|
|
|
void updateMyPosition(LatLng pos, double heading) {
|
|
_currentPosition = pos;
|
|
_currentHeading = heading;
|
|
}
|
|
|
|
Future<LocalNetworkTrackerService> init() async {
|
|
await _startListening();
|
|
_startBroadcasting();
|
|
_startCleanupRoutine();
|
|
return this;
|
|
}
|
|
|
|
/// 1. Start Listening for UDP Packets from other radios/tablets
|
|
Future<void> _startListening() async {
|
|
try {
|
|
_socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, broadcastPort);
|
|
_socket?.broadcastEnabled = true;
|
|
|
|
_socket?.listen((RawSocketEvent event) {
|
|
if (event == RawSocketEvent.read) {
|
|
final datagram = _socket?.receive();
|
|
if (datagram != null) {
|
|
_processIncomingPacket(datagram.data);
|
|
}
|
|
}
|
|
});
|
|
debugPrint('🟢 Local BFT Listener started on port $broadcastPort');
|
|
} catch (e) {
|
|
debugPrint('🔴 Local BFT Listener error: $e');
|
|
}
|
|
}
|
|
|
|
/// 2. Process incoming JSON position reports
|
|
void _processIncomingPacket(List<int> data) {
|
|
try {
|
|
final jsonStr = utf8.decode(data);
|
|
final map = jsonDecode(jsonStr);
|
|
final unit = FriendlyUnit.fromJson(map);
|
|
|
|
// Ignore our own broadcast loops
|
|
if (unit.deviceId == myDeviceId) return;
|
|
|
|
// Update the reactive map
|
|
friendlyUnits[unit.deviceId] = unit;
|
|
} catch (e) {
|
|
// Ignore malformed packets
|
|
}
|
|
}
|
|
|
|
/// 3. Broadcast our position to the local network every 3 seconds
|
|
void _startBroadcasting() {
|
|
_broadcastTimer = Timer.periodic(const Duration(seconds: 3), (timer) {
|
|
if (_currentPosition == null || _socket == null) return;
|
|
|
|
final me = FriendlyUnit(
|
|
deviceId: myDeviceId,
|
|
callsign: myCallsign,
|
|
position: _currentPosition!,
|
|
heading: _currentHeading,
|
|
role: myRole,
|
|
lastSeen: DateTime.now(),
|
|
);
|
|
|
|
final data = utf8.encode(jsonEncode(me.toJson()));
|
|
|
|
try {
|
|
// Send to universal broadcast address.
|
|
// In a real tactical mesh network (TrellisWare/Silvus),
|
|
// multicast or specific subnet broadcast is used.
|
|
_socket?.send(data, InternetAddress('255.255.255.255'), broadcastPort);
|
|
} catch (e) {
|
|
// Broadcast failed (e.g. no network interface active)
|
|
}
|
|
});
|
|
}
|
|
|
|
/// 4. Cleanup old units (Remove units not seen for 30 seconds)
|
|
void _startCleanupRoutine() {
|
|
_cleanupTimer = Timer.periodic(const Duration(seconds: 10), (timer) {
|
|
final now = DateTime.now();
|
|
final deadKeys = <String>[];
|
|
|
|
friendlyUnits.forEach((key, unit) {
|
|
if (now.difference(unit.lastSeen).inSeconds > 30) {
|
|
deadKeys.add(key);
|
|
}
|
|
});
|
|
|
|
for (var key in deadKeys) {
|
|
friendlyUnits.remove(key);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
_broadcastTimer?.cancel();
|
|
_cleanupTimer?.cancel();
|
|
_socket?.close();
|
|
super.onClose();
|
|
}
|
|
}
|