Added initial COT Sensor payloads to support UAV Tools integration for Arma Drones

This commit is contained in:
2026-04-13 08:03:31 -03:00
parent 753dcab26e
commit 0486f2a285
9 changed files with 376 additions and 12 deletions

View File

@@ -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";

View File

@@ -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";
};
};
};
};

View File

@@ -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;

View File

@@ -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 <OBJECT> 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]];

View File

@@ -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 <OBJECT> 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]];

View File

@@ -5,3 +5,4 @@ pub mod eud;
pub mod gps;
pub mod message;
pub mod nato;
pub mod uas;

243
src/cot/uas.rs Normal file
View File

@@ -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="<drone-uuid>"/>
// 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<UasVideoCoTPayload, FromArmaError> {
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 version=\"1.0\" encoding=\"UTF-8\" ?>");
xml.push_str(&format!(
"<event type=\"b-i-v\" version=\"2.0\" how=\"m-g\" \
uid=\"{uid}\" time=\"{now}\" start=\"{now}\" stale=\"{stale}\">",
uid = self.uid,
now = now,
stale = stale
));
// b-i-v events carry no real geographic position.
xml.push_str(
"<point lat=\"0\" lon=\"0\" hae=\"9999999.0\" \
ce=\"9999999.0\" le=\"9999999.0\"/>",
);
xml.push_str("<detail>");
xml.push_str("<__video>");
xml.push_str(&format!(
"<ConnectionEntry \
protocol=\"rtsp\" \
path=\"{path}\" \
address=\"{address}\" \
port=\"{port}\" \
uid=\"{uid}\" \
alias=\"{callsign}\" \
roverPort=\"-1\" \
rtspReliable=\"0\" \
ignoreEmbeddedKLV=\"False\" \
networkTimeout=\"0\" \
bufferTime=\"-1\"/>",
path = path,
address = address,
port = port,
uid = self.uid,
callsign = self.callsign,
));
xml.push_str("</__video>");
xml.push_str(&format!(
"<contact callsign=\"{}\"/>",
self.callsign
));
xml.push_str("</detail>");
xml.push_str("</event>");
xml
}
}
// ---------------------------------------------------------------------------
// b-m-p-s-p-loc Sensor position (FOV cone + video link)
// ---------------------------------------------------------------------------
pub struct UasSensorCoTPayload {
/// UID for this sensor event — conventionally "<drone-uuid>-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 (0359).
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<UasSensorCoTPayload, FromArmaError> {
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 version=\"1.0\" encoding=\"UTF-8\" ?>");
xml.push_str(&format!(
"<event type=\"b-m-p-s-p-loc\" version=\"2.0\" how=\"h-g-i-g-o\" \
uid=\"{uid}\" time=\"{now}\" start=\"{now}\" stale=\"{stale}\">",
uid = self.uid,
now = now,
stale = stale,
));
xml.push_str(&format!(
"<point lat=\"{lat}\" lon=\"{lon}\" hae=\"{hae}\" \
ce=\"9999999.0\" le=\"9999999.0\"/>",
lat = self.point_lat,
lon = self.point_lon,
hae = self.point_hae,
));
xml.push_str("<detail>");
// fovAlpha controls the transparency of the FOV cone fill (01).
// 0.537 ≈ 137/255, the value used by the real UAS Tool.
xml.push_str(&format!(
"<sensor \
fov=\"{fov}\" \
fovRed=\"1\" \
fovGreen=\"1\" \
fovBlue=\"1\" \
fovAlpha=\"0.5372549\" \
displayMagneticReference=\"0\" \
range=\"{range}\" \
azimuth=\"{az}\"/>",
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!(
"<contact callsign=\"{}\"/>",
self.callsign
));
xml.push_str("</detail>");
xml.push_str("</event>");
xml
}
}

View File

@@ -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",

View File

@@ -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"
}