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 friendlyUnits = {}.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 init() async { await _startListening(); _startBroadcasting(); _startCleanupRoutine(); return this; } /// 1. Start Listening for UDP Packets from other radios/tablets Future _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 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 = []; 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(); } }