diff --git a/addons/main/Cfg3den.hpp b/addons/main/Cfg3den.hpp index f49571f..4fbc719 100644 --- a/addons/main/Cfg3den.hpp +++ b/addons/main/Cfg3den.hpp @@ -78,12 +78,12 @@ class Cfg3den { condition = "objectVehicle"; typeName = "STRING"; }; - class armatak_attribute_marker_video_url { - displayName = "Video Feed URL"; - tooltip = "Video feed URL injected into __video on TAK"; - property = "armatak_attribute_marker_video_url"; + class armatak_attribute_video_url { + displayName = "Video URL (RTSP)"; + tooltip = "RTSP stream URL for UAS Tool integration. When set, the drone will appear in the ATAK UAS Tool with FOV cone and video feed. Format: rtsp://address:port/path (e.g. rtsp://192.168.1.10:8554/live/drone1). Leave empty to disable UAS Tool integration for this entity."; + property = "armatak_attribute_video_url"; control = "Edit"; - expression = "_this setVariable ['armatak_attribute_marker_video_url',_value]"; + expression = "_this setVariable ['armatak_attribute_video_url',_value]"; defaultValue = "''"; condition = "objectVehicle"; typeName = "STRING"; diff --git a/addons/main/CfgFunctions.hpp b/addons/main/CfgFunctions.hpp index e69bbdc..80b0383 100644 --- a/addons/main/CfgFunctions.hpp +++ b/addons/main/CfgFunctions.hpp @@ -19,6 +19,12 @@ class CfgFunctions { class send_marker_cot { file = "\armatak\armatak\addons\main\functions\api\fn_send_marker_cot.sqf"; }; + class send_uas_video_cot { + file = "\armatak\armatak\addons\main\functions\api\fn_send_uas_video_cot.sqf"; + }; + class send_uas_sensor_cot { + file = "\armatak\armatak\addons\main\functions\api\fn_send_uas_sensor_cot.sqf"; + }; class stop_tcp_socket { file = "\armatak\armatak\addons\main\functions\api\fn_stop_tcp_socket.sqf"; }; @@ -31,9 +37,6 @@ class CfgFunctions { class extract_marker_callsign { file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_marker_callsign.sqf"; }; - class extract_marker_video_url { - file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_marker_video_url.sqf"; - }; class extract_role { file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_role.sqf"; }; @@ -125,9 +128,6 @@ class CfgFunctions { class convert_to_rut_mandol { file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_rut_mandol.sqf"; }; - class convert_to_hellanmaa { - file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_hellanmaa.sqf"; - }; }; }; }; diff --git a/addons/main/functions/api/fn_send_drone_cot.sqf b/addons/main/functions/api/fn_send_drone_cot.sqf index e8c09a5..06764bf 100644 --- a/addons/main/functions/api/fn_send_drone_cot.sqf +++ b/addons/main/functions/api/fn_send_drone_cot.sqf @@ -32,3 +32,6 @@ if (!isNil "_pre_defined_role") then { }; _cot = [_drone, _atak_role, _atak_callsign] call armatak_fnc_send_marker_cot; + +[_drone] call armatak_fnc_send_uas_video_cot; +[_drone] call armatak_fnc_send_uas_sensor_cot; diff --git a/addons/main/functions/api/fn_send_uas_sensor_cot.sqf b/addons/main/functions/api/fn_send_uas_sensor_cot.sqf new file mode 100644 index 0000000..8b6517b --- /dev/null +++ b/addons/main/functions/api/fn_send_uas_sensor_cot.sqf @@ -0,0 +1,55 @@ +// function name: armatak_fnc_send_uas_sensor_cot +// function author: Valmo / ArmaTAK contributors +// function description: +// Sends a b-m-p-s-p-loc CoT event every router tick (1 s) for a drone. +// This is the "sensor position" event consumed by the ATAK UAS Tool to: +// - Draw the FOV cone on the moving map. +// - Compute four-corners for AR marker overlay on the video feed. +// - Show the SPoI (Sensor Point of Interest) crosshair. +// +// The event references the drone's b-i-v video endpoint via the drone UUID, +// so armatak_fnc_send_uas_video_cot must also be called for the same drone. +// +// Exits silently when "armatak_attribute_video_url" is not set, which keeps +// the behavior identical to the old fn_send_drone_cot for drones without a +// configured video stream. +// +// Arguments: +// 0: _drone The drone object. +// +// Return value: none + +params ["_drone"]; + +private _video_url = _drone getVariable ["armatak_attribute_video_url", ""]; +if (_video_url == "") exitWith {}; + +private _uuid = _drone call armatak_fnc_extract_uuid; +private _sensor_uid = _uuid + "-sensor"; +private _callsign = [_drone] call armatak_fnc_extract_marker_callsign; + +private _pos = (getPos _drone) call armatak_client_fnc_convertClientLocation; +private _lat = _pos select 0; +private _lon = _pos select 1; +private _hae = _pos select 2; + +private _azimuth = parseNumber ((getDir _drone) toFixed 0); + +private _allTurrets = [_drone, false] call BIS_fnc_allTurrets; +if (count _allTurrets > 0) then { + private _firstTurretPath = _allTurrets select 0; + private _turretWeapons = _drone weaponsTurret _firstTurretPath; + if (_turretWeapons isNotEqualTo []) then { + private _tDir = _drone weaponDirection (_turretWeapons select 0); + if (!((_tDir select 0) == 0 && (_tDir select 1) == 0)) then { + _azimuth = round (((_tDir select 0) atan2 (_tDir select 1) + 360) mod 360); + }; + }; +}; + +private _fov = _drone getVariable ["armatak_uas_fov", 60]; + +private _range = round (((getPosATL _drone) select 2) max 1); + +private _payload = [_sensor_uid, _uuid, _callsign, _lat, _lon, _hae, _azimuth, _fov, _range]; +"armatak" callExtension ["tcp_socket:cot:uas_sensor", [_payload]]; diff --git a/addons/main/functions/api/fn_send_uas_video_cot.sqf b/addons/main/functions/api/fn_send_uas_video_cot.sqf new file mode 100644 index 0000000..6fe25ef --- /dev/null +++ b/addons/main/functions/api/fn_send_uas_video_cot.sqf @@ -0,0 +1,30 @@ +// function name: armatak_fnc_send_uas_video_cot +// function author: Valmo / ArmaTAK contributors +// function description: +// Sends a b-i-v CoT event that declares the RTSP video endpoint for a drone. +// The ATAK UAS Tool picks this up and shows the drone in its UAS list with +// the associated video feed available for playback. +// +// The drone entity MUST have the variable "armatak_attribute_video_url" set +// to a valid RTSP URL, e.g.: +// _drone setVariable ["armatak_attribute_video_url", "rtsp://192.168.1.10:8554/live/drone1"]; +// or via the 3DEN attribute "Video URL (RTSP)" in the ARMA Team Awareness Kit +// attribute category. +// +// If the variable is absent or empty the function exits silently. +// +// Arguments: +// 0: _drone The drone object. +// +// Return value: none + +params ["_drone"]; + +private _video_url = _drone getVariable ["armatak_attribute_video_url", ""]; +if (_video_url == "") exitWith {}; + +private _uuid = _drone call armatak_fnc_extract_uuid; +private _callsign = [_drone] call armatak_fnc_extract_marker_callsign; + +private _payload = [_uuid, _callsign, _video_url]; +"armatak" callExtension ["tcp_socket:cot:uas_video", [_payload]]; diff --git a/src/cot/mod.rs b/src/cot/mod.rs index 1f23edf..db723df 100644 --- a/src/cot/mod.rs +++ b/src/cot/mod.rs @@ -5,3 +5,4 @@ pub mod eud; pub mod gps; pub mod message; pub mod nato; +pub mod uas; diff --git a/src/cot/uas.rs b/src/cot/uas.rs new file mode 100644 index 0000000..3702af3 --- /dev/null +++ b/src/cot/uas.rs @@ -0,0 +1,243 @@ +// src/cot/uas.rs +// +// CoT types required for ATAK UAS Tool integration. +// +// Two event types are needed so that the UAS Tool plugin recognises a drone: +// +// b-i-v — Video endpoint declaration. Tells the UAS Tool where +// to pull the RTSP stream for this drone. +// +// b-m-p-s-p-loc — Sensor position event. Carries the camera azimuth, +// field-of-view, and slant-range that the UAS Tool uses +// to draw the FOV cone on the map and to project AR +// markers onto the video feed. +// +// The two events are linked: the b-m-p-s-p-loc detail contains +// <__video uid=""/> +// which references the uid of the b-i-v event, so the UAS Tool knows which +// video stream belongs to this sensor. + +use arma_rs::{FromArma, FromArmaError}; +use chrono::{Duration, SecondsFormat, Utc}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Parse an RTSP URL of the form rtsp://address:port/path +/// into its three components. +fn parse_rtsp_url(url: &str) -> Option<(String, String, String)> { + let without_proto = url.strip_prefix("rtsp://")?; + let slash_pos = without_proto.find('/')?; + let host_port = &without_proto[..slash_pos]; + let path = &without_proto[slash_pos..]; // includes the leading '/' + let colon_pos = host_port.rfind(':')?; + let address = host_port[..colon_pos].to_string(); + let port = host_port[colon_pos + 1..].to_string(); + Some((address, port, path.to_string())) +} + +// --------------------------------------------------------------------------- +// b-i-v – Video endpoint declaration +// --------------------------------------------------------------------------- + +pub struct UasVideoCoTPayload { + /// The drone's persistent ATAK UUID (same uid used for PPLI / marker CoT). + pub uid: String, + /// Human-readable label shown in the UAS Tool video list. + pub callsign: String, + /// Full RTSP URL, e.g. "rtsp://192.168.1.10:8554/live/drone1". + pub video_url: String, +} + +impl FromArma for UasVideoCoTPayload { + fn from_arma(data: String) -> Result { + let (uid, callsign, video_url) = + <(String, String, String)>::from_arma(data)?; + Ok(Self { + uid, + callsign, + video_url, + }) + } +} + +impl UasVideoCoTPayload { + /// Build the complete XML string for the b-i-v CoT event. + /// Returns an empty string if the RTSP URL cannot be parsed. + pub fn to_xml(&self) -> String { + let (address, port, path) = match parse_rtsp_url(&self.video_url) { + Some(parts) => parts, + None => { + log::warn!( + "UasVideoCoTPayload: could not parse RTSP URL: {}", + self.video_url + ); + return String::new(); + } + }; + + let now = + Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true); + // Long stale time: the video endpoint is considered valid for 1 hour. + // The CoT is re-sent every router tick so it stays fresh even if the + // TAK server restarts. + let stale = (Utc::now() + Duration::seconds(3600)) + .to_rfc3339_opts(SecondsFormat::Millis, true); + + let mut xml = String::new(); + xml.push_str(""); + xml.push_str(&format!( + "", + uid = self.uid, + now = now, + stale = stale + )); + // b-i-v events carry no real geographic position. + xml.push_str( + "", + ); + xml.push_str(""); + xml.push_str("<__video>"); + xml.push_str(&format!( + "", + path = path, + address = address, + port = port, + uid = self.uid, + callsign = self.callsign, + )); + xml.push_str(""); + xml.push_str(&format!( + "", + self.callsign + )); + xml.push_str(""); + xml.push_str(""); + xml + } +} + +// --------------------------------------------------------------------------- +// b-m-p-s-p-loc – Sensor position (FOV cone + video link) +// --------------------------------------------------------------------------- + +pub struct UasSensorCoTPayload { + /// UID for this sensor event — conventionally "-sensor". + pub uid: String, + /// The drone's ATAK UUID; must match the uid used in the b-i-v event so + /// the UAS Tool can link sensor data to the correct video stream. + pub video_uid: String, + /// Callsign shown in the UAS Tool sensor list. + pub callsign: String, + /// Drone latitude in decimal degrees (WGS-84). + pub point_lat: f64, + /// Drone longitude in decimal degrees (WGS-84). + pub point_lon: f64, + /// Drone height above ellipsoid in metres (WGS-84). + pub point_hae: f32, + /// Camera azimuth in degrees, clockwise from true North (0–359). + pub azimuth: i32, + /// Camera horizontal field of view in degrees. + pub fov: i32, + /// Estimated slant range from drone to ground point in metres. + /// A good approximation is the drone's AGL altitude. + pub range: i32, +} + +impl FromArma for UasSensorCoTPayload { + fn from_arma(data: String) -> Result { + let ( + uid, + video_uid, + callsign, + point_lat, + point_lon, + point_hae, + azimuth, + fov, + range, + ) = <(String, String, String, f64, f64, f32, i32, i32, i32)>::from_arma( + data, + )?; + Ok(Self { + uid, + video_uid, + callsign, + point_lat, + point_lon, + point_hae, + azimuth, + fov, + range, + }) + } +} + +impl UasSensorCoTPayload { + /// Build the complete XML string for the b-m-p-s-p-loc CoT event. + pub fn to_xml(&self) -> String { + let now = + Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true); + // 60-second stale: must be refreshed every router tick (1 s) to keep + // the FOV cone visible on the map. + let stale = (Utc::now() + Duration::seconds(60)) + .to_rfc3339_opts(SecondsFormat::Millis, true); + + let mut xml = String::new(); + xml.push_str(""); + xml.push_str(&format!( + "", + uid = self.uid, + now = now, + stale = stale, + )); + xml.push_str(&format!( + "", + lat = self.point_lat, + lon = self.point_lon, + hae = self.point_hae, + )); + xml.push_str(""); + // fovAlpha controls the transparency of the FOV cone fill (0–1). + // 0.537 ≈ 137/255, the value used by the real UAS Tool. + xml.push_str(&format!( + "", + fov = self.fov, + range = self.range, + az = self.azimuth, + )); + // Link this sensor event to the b-i-v video endpoint. + xml.push_str(&format!("<__video uid=\"{}\"/>", self.video_uid)); + xml.push_str(&format!( + "", + self.callsign + )); + xml.push_str(""); + xml.push_str(""); + xml + } +} diff --git a/src/lib.rs b/src/lib.rs index f3cfb53..6a97b9a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,7 +61,10 @@ pub fn init() -> Extension { .command("eud", tcp::cot::send_eud_cot) .command("marker", tcp::cot::send_marker_cot) .command("digital_pointer", tcp::cot::send_digital_pointer_cot) - .command("chat", tcp::cot::send_message_cot), + .command("chat", tcp::cot::send_message_cot) + // UAS Tool integration + .command("uas_video", tcp::cot::send_uas_video_cot) + .command("uas_sensor", tcp::cot::send_uas_sensor_cot), ) .group( "draw", diff --git a/src/tcp/cot.rs b/src/tcp/cot.rs index a61a82f..e0c54bb 100644 --- a/src/tcp/cot.rs +++ b/src/tcp/cot.rs @@ -39,3 +39,32 @@ pub fn send_message_cot( "Sending Message CoT to TCP server" } + +/// Send a b-i-v CoT that declares the RTSP video endpoint for a drone. +/// Called by SQF via: "armatak" callExtension ["tcp_socket:cot:uas_video", [payload]] +/// +/// Returns early without sending if the RTSP URL in the payload cannot be parsed. +pub fn send_uas_video_cot( + ctx: Context, + payload: cot::uas::UasVideoCoTPayload, +) -> &'static str { + let xml = payload.to_xml(); + if !xml.is_empty() { + send_payload(ctx, xml); + } + + "Sending UAS Video (b-i-v) CoT to TCP server" +} + +/// Send a b-m-p-s-p-loc CoT carrying the drone camera's azimuth, FOV, and +/// slant-range so the UAS Tool can draw the FOV cone on the map. +/// Called by SQF via: "armatak" callExtension ["tcp_socket:cot:uas_sensor", [payload]] +pub fn send_uas_sensor_cot( + ctx: Context, + payload: cot::uas::UasSensorCoTPayload, +) -> &'static str { + let xml = payload.to_xml(); + send_payload(ctx, xml); + + "Sending UAS Sensor (b-m-p-s-p-loc) CoT to TCP server" +}