Files
maps-saas/packages/tactical_app/lib/services/local_network_tracker.dart
T

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();
}
}