mirror of
https://github.com/valmojr/armatak.git
synced 2026-06-13 19:43:29 +00:00
Compare commits
6 Commits
v1.3.0
...
uav_tool_i
| Author | SHA1 | Date | |
|---|---|---|---|
| 0486f2a285 | |||
| 753dcab26e | |||
| 2f53488ba8 | |||
| 323339e679 | |||
| 3f14a75e81 | |||
| 469a54c141 |
@@ -107,6 +107,12 @@ switch (toLower worldName) do {
|
|||||||
case "rut_mandol": {
|
case "rut_mandol": {
|
||||||
_realLocation = _position call armatak_fnc_convert_to_rut_mandol;
|
_realLocation = _position call armatak_fnc_convert_to_rut_mandol;
|
||||||
};
|
};
|
||||||
|
case "hellanmaa": {
|
||||||
|
_realLocation = _position call armatak_fnc_convert_to_hellanmaa;
|
||||||
|
};
|
||||||
|
case "hellanmaaw": {
|
||||||
|
_realLocation = _position call armatak_fnc_convert_to_hellanmaa;
|
||||||
|
};
|
||||||
default {
|
default {
|
||||||
_warning = format ["<t color='#FF8021'>ARMATAK</t><br/> %1", "Unsupported Map"];
|
_warning = format ["<t color='#FF8021'>ARMATAK</t><br/> %1", "Unsupported Map"];
|
||||||
[[_warning, 1.5]] call CBA_fnc_notify;
|
[[_warning, 1.5]] call CBA_fnc_notify;
|
||||||
|
|||||||
@@ -78,6 +78,16 @@ class Cfg3den {
|
|||||||
condition = "objectVehicle";
|
condition = "objectVehicle";
|
||||||
typeName = "STRING";
|
typeName = "STRING";
|
||||||
};
|
};
|
||||||
|
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_video_url',_value]";
|
||||||
|
defaultValue = "''";
|
||||||
|
condition = "objectVehicle";
|
||||||
|
typeName = "STRING";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ class CfgFunctions {
|
|||||||
class send_marker_cot {
|
class send_marker_cot {
|
||||||
file = "\armatak\armatak\addons\main\functions\api\fn_send_marker_cot.sqf";
|
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 {
|
class stop_tcp_socket {
|
||||||
file = "\armatak\armatak\addons\main\functions\api\fn_stop_tcp_socket.sqf";
|
file = "\armatak\armatak\addons\main\functions\api\fn_stop_tcp_socket.sqf";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,3 +32,6 @@ if (!isNil "_pre_defined_role") then {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_cot = [_drone, _atak_role, _atak_callsign] call armatak_fnc_send_marker_cot;
|
_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;
|
||||||
|
|||||||
@@ -5,9 +5,10 @@
|
|||||||
params ["_unit", "_type", "_callsign"];
|
params ["_unit", "_type", "_callsign"];
|
||||||
|
|
||||||
_unit_position = _unit call armatak_client_fnc_extractClientPosition;
|
_unit_position = _unit call armatak_client_fnc_extractClientPosition;
|
||||||
|
_video_url = [_unit] call armatak_fnc_extract_marker_video_url;
|
||||||
|
|
||||||
_uuid = _unit call armatak_fnc_extract_uuid;
|
_uuid = _unit call armatak_fnc_extract_uuid;
|
||||||
|
|
||||||
_marker_cot = [_uuid, _type, _unit_position select 1, _unit_position select 2, _unit_position select 3, _callsign, _unit_position select 5, _unit_position select 6];
|
_marker_cot = [_uuid, _type, _unit_position select 1, _unit_position select 2, _unit_position select 3, _callsign, _unit_position select 5, _unit_position select 6, _video_url];
|
||||||
|
|
||||||
"armatak" callExtension ["tcp_socket:cot:marker", [_marker_cot]];
|
"armatak" callExtension ["tcp_socket:cot:marker", [_marker_cot]];
|
||||||
|
|||||||
55
addons/main/functions/api/fn_send_uas_sensor_cot.sqf
Normal file
55
addons/main/functions/api/fn_send_uas_sensor_cot.sqf
Normal 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]];
|
||||||
30
addons/main/functions/api/fn_send_uas_video_cot.sqf
Normal file
30
addons/main/functions/api/fn_send_uas_video_cot.sqf
Normal 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]];
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// function name: armatak_fnc_extract_marker_video_url
|
||||||
|
// function author: Codex
|
||||||
|
// function description: Gets the marker video URL configured in 3DEN for a vehicle
|
||||||
|
|
||||||
|
params ["_unit"];
|
||||||
|
|
||||||
|
private _videoUrl = _unit getVariable ["armatak_attribute_marker_video_url", ""];
|
||||||
|
|
||||||
|
if (isNil "_videoUrl") exitWith {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
_videoUrl
|
||||||
30
addons/main/functions/map/fn_convert_to_hellanmaa.sqf
Normal file
30
addons/main/functions/map/fn_convert_to_hellanmaa.sqf
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
params ["_longitudeInGame", "_latitudeInGame", "_altitude"];
|
||||||
|
|
||||||
|
private _mapWidth = 8192;
|
||||||
|
private _mapHeight = 8192;
|
||||||
|
|
||||||
|
// SW corner (used as origin)
|
||||||
|
private _SW_lat = 63.005389;
|
||||||
|
private _SW_lon = 22.638957;
|
||||||
|
|
||||||
|
// SE corner
|
||||||
|
private _SE_lat = 63.010092;
|
||||||
|
private _SE_lon = 22.800107;
|
||||||
|
|
||||||
|
// NW corner
|
||||||
|
private _NW_lat = 63.078713;
|
||||||
|
private _NW_lon = 22.628542;
|
||||||
|
|
||||||
|
private _edgeSE_lat = _SE_lat - _SW_lat;
|
||||||
|
private _edgeSE_lon = _SE_lon - _SW_lon;
|
||||||
|
|
||||||
|
private _edgeNW_lat = _NW_lat - _SW_lat;
|
||||||
|
private _edgeNW_lon = _NW_lon - _SW_lon;
|
||||||
|
|
||||||
|
private _fx = _longitudeInGame / _mapWidth;
|
||||||
|
private _fy = _latitudeInGame / _mapHeight;
|
||||||
|
|
||||||
|
private _realLat = _SW_lat + (_fx * _edgeSE_lat) + (_fy * _edgeNW_lat);
|
||||||
|
private _realLon = _SW_lon + (_fx * _edgeSE_lon) + (_fy * _edgeNW_lon);
|
||||||
|
|
||||||
|
[_realLat, _realLon, _altitude]
|
||||||
@@ -1 +0,0 @@
|
|||||||
armatak\armatak\addons\video
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
class Extended_PreStart_EventHandlers {
|
|
||||||
class ADDON {
|
|
||||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
class Extended_PreInit_EventHandlers {
|
|
||||||
class ADDON {
|
|
||||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
class CfgVehicles {
|
|
||||||
class Logic;
|
|
||||||
|
|
||||||
class Module_F : Logic
|
|
||||||
{
|
|
||||||
class AttributesBase
|
|
||||||
{
|
|
||||||
class Default;
|
|
||||||
class Edit;
|
|
||||||
class Combo;
|
|
||||||
class Checkbox;
|
|
||||||
class CheckboxNumber;
|
|
||||||
class ModuleDescription;
|
|
||||||
class Units;
|
|
||||||
};
|
|
||||||
|
|
||||||
class ModuleDescription
|
|
||||||
{
|
|
||||||
class AnyBrain;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
class EGVAR(server,moduleBase);
|
|
||||||
class GVAR(videoModule): EGVAR(server,moduleBase) {
|
|
||||||
scope = 2;
|
|
||||||
scopeCurator = 0;
|
|
||||||
displayname = "Video Streaming Handler";
|
|
||||||
icon = "\a3\Modules_F_Curator\Data\iconRadio_ca.paa";
|
|
||||||
category = QEGVAR(main,moduleCategory);
|
|
||||||
function = QFUNC(videoParser);
|
|
||||||
functionPriority = 1;
|
|
||||||
isGlobal = 0;
|
|
||||||
isTriggerActivated = 1;
|
|
||||||
isDisposable = 1;
|
|
||||||
is3den = 0;
|
|
||||||
curatorCanAttach = 0;
|
|
||||||
curatorInfoType = "RscDisplayAttributeModuleNuke";
|
|
||||||
canSetArea = 0;
|
|
||||||
canSetAreaShape = 0;
|
|
||||||
canSetAreaHeight = 0;
|
|
||||||
/*
|
|
||||||
class Attributes: AttributesBase {
|
|
||||||
class GVAR(instanceAddress): Edit {
|
|
||||||
property = QGVAR(instanceAddress);
|
|
||||||
displayname = "MediaMTX Provider Address";
|
|
||||||
tooltip = "MediaMTX Provider Instance Address";
|
|
||||||
typeName = "STRING";
|
|
||||||
defaultValue = "localhost";
|
|
||||||
};
|
|
||||||
class GVAR(instancePort): Edit {
|
|
||||||
property = QGVAR(instancePort);
|
|
||||||
displayname = QUOTE(MediaMTX Provider Port);
|
|
||||||
tooltip = QUOTE(MediaMTX Provider Port for handling video streams);
|
|
||||||
typeName = "STRING";
|
|
||||||
defaultValue = "8554";
|
|
||||||
};
|
|
||||||
class GVAR(instanceAuthUser): Edit {
|
|
||||||
property = QGVAR(instanceAuthUser);
|
|
||||||
displayname = QUOTE(MediaMTX Provider Username);
|
|
||||||
tooltip = QUOTE(MediaMTX Provider Instance Username);
|
|
||||||
typeName = "STRING";
|
|
||||||
defaultValue = "administrator";
|
|
||||||
};
|
|
||||||
class GVAR(instanceAuthPassword): Edit {
|
|
||||||
property = QGVAR(instanceAuthPassword);
|
|
||||||
displayname = QUOTE(MediaMTX Provider Password);
|
|
||||||
tooltip = QUOTE(MediaMTX Provider Instance Password);
|
|
||||||
typeName = "STRING";
|
|
||||||
defaultValue = "password";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
PREP(videoParser);
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#include "script_component.hpp"
|
|
||||||
|
|
||||||
ADDON = false;
|
|
||||||
|
|
||||||
PREP_RECOMPILE_START;
|
|
||||||
#include "XEH_PREP.hpp"
|
|
||||||
PREP_RECOMPILE_END;
|
|
||||||
|
|
||||||
ADDON = true;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
#include "script_component.hpp"
|
|
||||||
|
|
||||||
#include "XEH_PREP.hpp"
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#include "script_component.hpp"
|
|
||||||
|
|
||||||
class CfgPatches {
|
|
||||||
class ADDON {
|
|
||||||
name = COMPONENT_NAME;
|
|
||||||
units[] = {
|
|
||||||
//QGVAR(videoModule)
|
|
||||||
};
|
|
||||||
weapons[] = {};
|
|
||||||
requiredAddons[] = {
|
|
||||||
"cba_main",
|
|
||||||
"ace_main",
|
|
||||||
"armatak_main",
|
|
||||||
"armatak_server"
|
|
||||||
};
|
|
||||||
requiredVersion = REQUIRED_VERSION;
|
|
||||||
author = PROJECT_AUTHOR;
|
|
||||||
url = "https://github.com/valmojr/armatak";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
#include "CfgEventHandlers.hpp"
|
|
||||||
//#include "CfgVehicles.hpp"
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
#include "..\script_component.hpp"
|
|
||||||
|
|
||||||
params [
|
|
||||||
["_logic", objNull, [objNull]],
|
|
||||||
["_units", [], [[]]],
|
|
||||||
["_activated", true, [true]]
|
|
||||||
];
|
|
||||||
|
|
||||||
if (isServer) exitWith {
|
|
||||||
private _instance_address = GETVAR(_logic,GVAR(instanceAddress),false);
|
|
||||||
private _instance_port = GETVAR(_logic,GVAR(instancePort),false);
|
|
||||||
private _instance_auth_user = GETVAR(_logic,GVAR(instanceAuthUser),false);
|
|
||||||
private _instance_auth_pass = GETVAR(_logic,GVAR(instanceAuthPassword),false);
|
|
||||||
|
|
||||||
SETMVAR(GVAR(instanceAddress),_instance_address);
|
|
||||||
SETMVAR(GVAR(instancePort),_instance_port);
|
|
||||||
SETMVAR(GVAR(instanceAuthUser),_instance_auth_user);
|
|
||||||
SETMVAR(GVAR(instanceAuthPassword),_instance_auth_pass);
|
|
||||||
|
|
||||||
_startAction = [
|
|
||||||
QGVAR(startStream),
|
|
||||||
"Start Video Feed",
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
_uuid = (_this select 0) call armatak_fnc_extract_uuid;
|
|
||||||
_uuid_short = _uuid select [0, 8];
|
|
||||||
_role = roleDescription (_this select 0);
|
|
||||||
_name = name (_this select 0);
|
|
||||||
|
|
||||||
_role = [_role] call BIS_fnc_filterString;
|
|
||||||
_name = [_name] call BIS_fnc_filterString;
|
|
||||||
|
|
||||||
_stream_path = _name + "_" + _role + "_" + _uuid_short;
|
|
||||||
|
|
||||||
armatak_mediamtx_video_stream_instance_address = GETMVAR(instance_address,false);
|
|
||||||
armatak_mediamtx_video_stream_instance_port = missionNamespace getVariable "instance_port";
|
|
||||||
armatak_mediamtx_video_stream_instance_auth_user = missionNamespace getVariable "instance_auth_user";
|
|
||||||
armatak_mediamtx_video_stream_instance_auth_pass = missionNamespace getVariable "instance_auth_pass";
|
|
||||||
|
|
||||||
"armatak" callExtension ["video_stream:start", [armatak_mediamtx_video_stream_instance_address, armatak_mediamtx_video_stream_instance_port, _stream_path, armatak_mediamtx_video_stream_instance_auth_user, armatak_mediamtx_video_stream_instance_auth_pass]];
|
|
||||||
(_this select 0) setVariable ["armatak_video_feed_is_streaming", true];
|
|
||||||
},
|
|
||||||
{
|
|
||||||
(_this select 0) getVariable "armatak_video_feed_is_streaming" == false
|
|
||||||
}
|
|
||||||
] call ace_interact_menu_fnc_createAction;
|
|
||||||
[
|
|
||||||
"Man",
|
|
||||||
1,
|
|
||||||
["ACE_SelfActions"],
|
|
||||||
_startAction,
|
|
||||||
true
|
|
||||||
] call ace_interact_menu_fnc_addActionToClass;
|
|
||||||
|
|
||||||
_stopAction = [
|
|
||||||
"ArmatakStopStream",
|
|
||||||
"Stop Video Feed",
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
"armatak" callExtension ["video_stream:stop", []];
|
|
||||||
SETVAR(_this select 0,GVAR(isStreaming),false);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
GETVAR((this select 0),GVAR(isStreaming),false)
|
|
||||||
}
|
|
||||||
] call ace_interact_menu_fnc_createAction;
|
|
||||||
[
|
|
||||||
"Man",
|
|
||||||
1,
|
|
||||||
["ACE_SelfActions"],
|
|
||||||
_stopAction,
|
|
||||||
true
|
|
||||||
] call ace_interact_menu_fnc_addActionToClass;
|
|
||||||
if (isMultiplayer) then {
|
|
||||||
{
|
|
||||||
SETVAR(_x,GVAR(isStreaming),false);
|
|
||||||
} forEach playableUnits;
|
|
||||||
} else {
|
|
||||||
SETVAR(player,GVAR(isStreaming),false);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
true;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#define COMPONENT video
|
|
||||||
#define COMPONENT_BEAUTIFIED Video Streaming
|
|
||||||
#include "\armatak\armatak\addons\main\script_mod.hpp"
|
|
||||||
|
|
||||||
// #define DEBUG_MODE_FULL
|
|
||||||
// #define DISABLE_COMPILE_CACHE
|
|
||||||
// #define ENABLE_PERFORMANCE_COUNTERS
|
|
||||||
|
|
||||||
#ifdef DEBUG_ENABLED_MAIN
|
|
||||||
#define DEBUG_MODE_FULL
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef DEBUG_SETTINGS_MAIN
|
|
||||||
#define DEBUG_SETTINGS DEBUG_SETTINGS_MAIN
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "\z\ace\addons\main\script_macros.hpp"
|
|
||||||
@@ -16,9 +16,18 @@ pub struct CursorOverTime {
|
|||||||
pub track_speed: Option<f32>,
|
pub track_speed: Option<f32>,
|
||||||
pub link_uid: Option<String>,
|
pub link_uid: Option<String>,
|
||||||
pub remarker: Option<String>,
|
pub remarker: Option<String>,
|
||||||
|
pub video_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CursorOverTime {
|
impl CursorOverTime {
|
||||||
|
fn escape_xml_attribute(value: &str) -> String {
|
||||||
|
value
|
||||||
|
.replace('&', "&")
|
||||||
|
.replace('"', """)
|
||||||
|
.replace('<', "<")
|
||||||
|
.replace('>', ">")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn convert_to_xml(&self) -> String {
|
pub fn convert_to_xml(&self) -> String {
|
||||||
let uuid = match &self.uuid {
|
let uuid = match &self.uuid {
|
||||||
Some(uuid) => uuid,
|
Some(uuid) => uuid,
|
||||||
@@ -107,6 +116,18 @@ impl CursorOverTime {
|
|||||||
xml.push_str(format!("<remarks>ARMATAK | {}</remarks>", remark).as_str());
|
xml.push_str(format!("<remarks>ARMATAK | {}</remarks>", remark).as_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(video_url) = &self.video_url {
|
||||||
|
if !video_url.trim().is_empty() {
|
||||||
|
xml.push_str(
|
||||||
|
format!(
|
||||||
|
"<__video url=\"{}\" />",
|
||||||
|
Self::escape_xml_attribute(video_url.trim())
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
xml.push_str("</detail></event>");
|
xml.push_str("</detail></event>");
|
||||||
|
|
||||||
return xml;
|
return xml;
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ impl DigitalPointerPayload {
|
|||||||
track_speed: None,
|
track_speed: None,
|
||||||
link_uid: Some(self.link_uid.clone()),
|
link_uid: Some(self.link_uid.clone()),
|
||||||
remarker: None,
|
remarker: None,
|
||||||
|
video_url: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ impl EudCoTPayload {
|
|||||||
track_speed: Some(self.track_speed),
|
track_speed: Some(self.track_speed),
|
||||||
link_uid: None,
|
link_uid: None,
|
||||||
remarker: None,
|
remarker: None,
|
||||||
|
video_url: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ impl ExternalPositionPayload {
|
|||||||
track_speed: Some(self.track_speed),
|
track_speed: Some(self.track_speed),
|
||||||
link_uid: None,
|
link_uid: None,
|
||||||
remarker: Some(self.remarker.clone()),
|
remarker: Some(self.remarker.clone()),
|
||||||
|
video_url: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ pub mod eud;
|
|||||||
pub mod gps;
|
pub mod gps;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod nato;
|
pub mod nato;
|
||||||
|
pub mod uas;
|
||||||
|
|||||||
@@ -11,10 +11,40 @@ pub struct MarkerCoTPayload {
|
|||||||
pub contact_callsign: String,
|
pub contact_callsign: String,
|
||||||
pub track_course: i32,
|
pub track_course: i32,
|
||||||
pub track_speed: f32,
|
pub track_speed: f32,
|
||||||
|
pub video_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromArma for MarkerCoTPayload {
|
impl FromArma for MarkerCoTPayload {
|
||||||
fn from_arma(data: String) -> Result<MarkerCoTPayload, FromArmaError> {
|
fn from_arma(data: String) -> Result<MarkerCoTPayload, FromArmaError> {
|
||||||
|
if let Ok((
|
||||||
|
uuid,
|
||||||
|
r#type,
|
||||||
|
point_lat,
|
||||||
|
point_lon,
|
||||||
|
point_hae,
|
||||||
|
contact_callsign,
|
||||||
|
track_course,
|
||||||
|
track_speed,
|
||||||
|
video_url,
|
||||||
|
)) = <(String, String, f64, f64, f32, String, i32, f32, String)>::from_arma(data.clone())
|
||||||
|
{
|
||||||
|
return Ok(Self {
|
||||||
|
uuid,
|
||||||
|
r#type,
|
||||||
|
point_lat,
|
||||||
|
point_lon,
|
||||||
|
point_hae,
|
||||||
|
contact_callsign,
|
||||||
|
track_course,
|
||||||
|
track_speed,
|
||||||
|
video_url: if video_url.trim().is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(video_url)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let (
|
let (
|
||||||
uuid,
|
uuid,
|
||||||
r#type,
|
r#type,
|
||||||
@@ -34,6 +64,7 @@ impl FromArma for MarkerCoTPayload {
|
|||||||
contact_callsign,
|
contact_callsign,
|
||||||
track_course,
|
track_course,
|
||||||
track_speed,
|
track_speed,
|
||||||
|
video_url: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,6 +86,7 @@ impl MarkerCoTPayload {
|
|||||||
track_speed: Some(self.track_speed),
|
track_speed: Some(self.track_speed),
|
||||||
link_uid: None,
|
link_uid: None,
|
||||||
remarker: None,
|
remarker: None,
|
||||||
|
video_url: self.video_url.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
243
src/cot/uas.rs
Normal file
243
src/cot/uas.rs
Normal 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 (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<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 (0–1).
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,7 +61,10 @@ pub fn init() -> Extension {
|
|||||||
.command("eud", tcp::cot::send_eud_cot)
|
.command("eud", tcp::cot::send_eud_cot)
|
||||||
.command("marker", tcp::cot::send_marker_cot)
|
.command("marker", tcp::cot::send_marker_cot)
|
||||||
.command("digital_pointer", tcp::cot::send_digital_pointer_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(
|
.group(
|
||||||
"draw",
|
"draw",
|
||||||
|
|||||||
@@ -39,3 +39,32 @@ pub fn send_message_cot(
|
|||||||
|
|
||||||
"Sending Message CoT to TCP server"
|
"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"
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user