chore: add build artifacts, map assets, and routing data
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user