From 6376b7acf0f6e6c6a3d55e5dfa4a4981ea9cfd30 Mon Sep 17 00:00:00 2001 From: Valmo Trindade Date: Thu, 7 May 2026 03:53:19 -0300 Subject: [PATCH] Refacted UDP Socket config and created UAS addon --- addons/client/dialog.hpp | 24 +++++- .../client/functions/fnc_startUDPSocket.sqf | 9 +- addons/main/Cfg3den.hpp | 2 +- addons/main/XEH_postInit.sqf | 31 +++++++ addons/uav/XEH_PREP.hpp | 3 + .../functions/fnc_handleMavlinkCallback.sqf | 63 ++++++++++++++ .../fnc_parseMavlinkCallbackData.sqf | 17 ++++ addons/uav/functions/fnc_resolveVideoUri.sqf | 37 ++++++++ .../functions/fnc_updateMavlinkBroadcast.sqf | 84 ++++++++++--------- 9 files changed, 225 insertions(+), 45 deletions(-) create mode 100644 addons/uav/functions/fnc_handleMavlinkCallback.sqf create mode 100644 addons/uav/functions/fnc_parseMavlinkCallbackData.sqf create mode 100644 addons/uav/functions/fnc_resolveVideoUri.sqf diff --git a/addons/client/dialog.hpp b/addons/client/dialog.hpp index 773cbd9..e424bcf 100644 --- a/addons/client/dialog.hpp +++ b/addons/client/dialog.hpp @@ -12,7 +12,7 @@ class armatak_udp_socket_start_dialog { x = "0.386562 * safezoneW + safezoneX"; y = "0.357 * safezoneH + safezoneY"; w = "0.216563 * safezoneW"; - h = "0.319 * safezoneH"; + h = "0.418 * safezoneH"; colorBackground[]={0,0,0,0.45}; }; }; @@ -44,6 +44,16 @@ class armatak_udp_socket_start_dialog { h = "0.044 * safezoneH"; colorBackground[]={0,0,0,0.5}; }; + class armatak_gui_module_udp_socket_dialog_video_feed_url_edit: RscEdit { + idc = 16969; + text = ""; + x = "0.391719 * safezoneW + safezoneX"; + y = "0.632 * safezoneH + safezoneY"; + w = "0.20625 * safezoneW"; + h = "0.044 * safezoneH"; + colorBackground[]={0,0,0,0.5}; + tooltip = "Optional shared feed URL. If empty, the UAV 3DEN URL is used first, then a local RTP fallback."; + }; class armatak_gui_module_udp_socket_dialog_address_text: RscText { idc = 16963; text = "EUD's Address"; @@ -68,12 +78,20 @@ class armatak_udp_socket_start_dialog { w = "0.20625 * safezoneW"; h = "0.033 * safezoneH"; }; + class armatak_gui_module_udp_socket_dialog_video_feed_url_text: RscText { + idc = 16970; + text = "Video Feed URL (Optional)"; + x = "0.391719 * safezoneW + safezoneX"; + y = "0.599 * safezoneH + safezoneY"; + w = "0.20625 * safezoneW"; + h = "0.033 * safezoneH"; + }; class armatak_gui_module_udp_socket_dialog_address_button_cancel: RscButton { idc = 16965; text = "Cancel"; action = "closeDialog 2;"; x = "0.551563 * safezoneW + safezoneX"; - y = "0.632 * safezoneH + safezoneY"; + y = "0.709 * safezoneH + safezoneY"; w = "0.0464063 * safezoneW"; h = "0.055 * safezoneH"; }; @@ -82,7 +100,7 @@ class armatak_udp_socket_start_dialog { text = "Ok"; action = QUOTE(call FUNC(startUDPSocket)); x = "0.5 * safezoneW + safezoneX"; - y = "0.632 * safezoneH + safezoneY"; + y = "0.709 * safezoneH + safezoneY"; w = "0.0464063 * safezoneW"; h = "0.055 * safezoneH"; }; diff --git a/addons/client/functions/fnc_startUDPSocket.sqf b/addons/client/functions/fnc_startUDPSocket.sqf index 3381eac..79def4c 100644 --- a/addons/client/functions/fnc_startUDPSocket.sqf +++ b/addons/client/functions/fnc_startUDPSocket.sqf @@ -14,19 +14,24 @@ disableSerialization; private _eud_address = ctrlText 16961; private _gnss_port = ctrlText 16962; private _mavlink_port = ctrlText 16967; +private _video_feed_url = ctrlText 16969; private _udp_socket_fulladdress = _eud_address + ":" + _gnss_port; private _mavlink_address = _eud_address + ":" + _mavlink_port; player setVariable [QGVAR(udp_socket_address), _udp_socket_fulladdress]; player setVariable [QGVAR(mavlink_address), _mavlink_address]; +player setVariable [QGVAR(video_feed_url), trim _video_feed_url]; player setVariable [QGVAR(eudConnected), true]; +private _advertised_video_uri = [objNull] call EFUNC(uav,resolveVideoUri); + "armatak" callExtension ["udp_socket:start", [_udp_socket_fulladdress]]; +"armatak" callExtension ["uas:start_endpoint", [parseNumber _mavlink_port]]; private _mdnsInstanceName = format ["ArmaTAK-%1", name player]; -"armatak" callExtension ["mdns:start_uas_advertisement", [_mdnsInstanceName, parseNumber _mavlink_port, "rtp://0.0.0.0:5600"]]; -"armatak" callExtension ["log", [["info", format ["Client UDP socket started for %1 and MAVLink target set to %2", _udp_socket_fulladdress, _mavlink_address]]]]; +"armatak" callExtension ["mdns:start_uas_advertisement", [_mdnsInstanceName, parseNumber _mavlink_port, _advertised_video_uri]]; +"armatak" callExtension ["log", [["info", format ["Client UDP socket started for %1, MAVLink target set to %2 and advertised video URI set to %3", _udp_socket_fulladdress, _mavlink_address, _advertised_video_uri]]]]; call EFUNC(uav,startMavlinkBroadcast); diff --git a/addons/main/Cfg3den.hpp b/addons/main/Cfg3den.hpp index f49571f..b8fb9aa 100644 --- a/addons/main/Cfg3den.hpp +++ b/addons/main/Cfg3den.hpp @@ -80,7 +80,7 @@ class Cfg3den { }; class armatak_attribute_marker_video_url { displayName = "Video Feed URL"; - tooltip = "Video feed URL injected into __video on TAK"; + tooltip = "Shared UAV video URL. This per-vehicle value overrides the optional session video URL from Connect to EUD."; property = "armatak_attribute_marker_video_url"; control = "Edit"; expression = "_this setVariable ['armatak_attribute_marker_video_url',_value]"; diff --git a/addons/main/XEH_postInit.sqf b/addons/main/XEH_postInit.sqf index 3475eaf..96f1ff1 100644 --- a/addons/main/XEH_postInit.sqf +++ b/addons/main/XEH_postInit.sqf @@ -14,6 +14,7 @@ addMissionEventHandler ["ExtensionCallback", { case "EUD Disconnected": { SETVAR(player,EGVAR(client,eudConnected),false); call EFUNC(uav,stopMavlinkBroadcast); + "armatak" callExtension ["uas:stop_endpoint", []]; "armatak" callExtension ["mdns:stop", []]; }; default {}; @@ -28,15 +29,45 @@ addMissionEventHandler ["ExtensionCallback", { if (_function == "UDP Socket is not running") then { SETVAR(player,EGVAR(client,eudConnected),false); call EFUNC(uav,stopMavlinkBroadcast); + "armatak" callExtension ["uas:stop_endpoint", []]; "armatak" callExtension ["mdns:stop", []]; }; if (_function == "failed to bind UDP socket") then { SETVAR(player,EGVAR(client,eudConnected),false); call EFUNC(uav,stopMavlinkBroadcast); + "armatak" callExtension ["uas:stop_endpoint", []]; "armatak" callExtension ["mdns:stop", []]; }; }; + case "MAVLINK UDP ERROR": { + _message = _function; + if (_data isNotEqualTo "") then { + _message = format ["%1: %2", _function, _data]; + }; + + [_message, "warning", _name] call FUNC(notify); + }; + case "MAVLINK UDP": { + private _history = missionNamespace getVariable ["armatak_uav_mavlink_callback_history", []]; + _history pushBack [diag_tickTime, _function, _data]; + if ((count _history) > 50) then { + _history deleteRange [0, (count _history) - 50]; + }; + missionNamespace setVariable ["armatak_uav_mavlink_callback_history", _history]; + missionNamespace setVariable ["armatak_uav_last_mavlink_callback", [diag_tickTime, _function, _data]]; + + switch (_function) do { + case "COMMAND_LONG"; + case "COMMAND_INT"; + case "COMMAND_ACK"; + case "MANUAL_CONTROL": { + "armatak" callExtension ["log", [["info", format ["MAVLINK UDP CALLBACK %1 %2", _function, _data]]]]; + [_function, _data] call EFUNC(uav,handleMavlinkCallback); + }; + default {}; + }; + }; case "TCP SOCKET": { [_function, "success", _name] call FUNC(notify); }; diff --git a/addons/uav/XEH_PREP.hpp b/addons/uav/XEH_PREP.hpp index e7715f6..78fdb7a 100644 --- a/addons/uav/XEH_PREP.hpp +++ b/addons/uav/XEH_PREP.hpp @@ -1,3 +1,6 @@ PREP(startMavlinkBroadcast); PREP(stopMavlinkBroadcast); PREP(updateMavlinkBroadcast); +PREP(resolveVideoUri); +PREP(handleMavlinkCallback); +PREP(parseMavlinkCallbackData); diff --git a/addons/uav/functions/fnc_handleMavlinkCallback.sqf b/addons/uav/functions/fnc_handleMavlinkCallback.sqf new file mode 100644 index 0000000..20f95ba --- /dev/null +++ b/addons/uav/functions/fnc_handleMavlinkCallback.sqf @@ -0,0 +1,63 @@ +#include "..\script_component.hpp" + +params ["_function", ["_data", "", [""]]]; + +if (!hasInterface) exitWith {}; + +private _payload = [_data] call FUNC(parseMavlinkCallbackData); +private _uav = getConnectedUAV player; +if (isNull _uav) exitWith { + "armatak" callExtension ["log", [["warn", format ["Ignoring MAVLINK UDP callback %1 because no UAV is connected: %2", _function, _data]]]]; +}; + +private _command = parseNumber (_payload getOrDefault ["command", "-1"]); +private _commandName = _payload getOrDefault ["command_name", "UNKNOWN"]; + +switch (_function) do { + case "COMMAND_LONG": { + switch (_command) do { + case 400: { + private _armValue = parseNumber (_payload getOrDefault ["param1", "0"]); + private _doArm = _armValue >= 1; + _uav engineOn _doArm; + if (_doArm) then { + _uav setFuel ((fuel _uav) max 0.1); + }; + systemChat format ["ATAK %1 %2", ["DISARM", "ARM"] select _doArm, [_uav] call armatak_fnc_extract_marker_callsign]; + "armatak" callExtension ["log", [["info", format ["Applied MAVLINK COMMAND_LONG %1 (%2) to UAV %3", _command, _commandName, _uav]]]]; + }; + case 22: { + _uav engineOn true; + _uav setFuel ((fuel _uav) max 0.1); + _uav flyInHeight 75; + + if (_uav isKindOf "Helicopter" || {_uav isKindOf "VTOL_Base_F"} || {_uav isKindOf "Quadbike_01_F"}) then { + private _velocity = velocityModelSpace _uav; + _uav setVelocityModelSpace [ + _velocity select 0, + (_velocity select 1) max 15, + ((_velocity select 2) max 0) + 8 + ]; + }; + + systemChat format ["ATAK TAKEOFF %1", [_uav] call armatak_fnc_extract_marker_callsign]; + "armatak" callExtension ["log", [["info", format ["Applied MAVLINK TAKEOFF to UAV %1", _uav]]]]; + }; + default { + "armatak" callExtension ["log", [["info", format ["Unhandled MAVLINK COMMAND_LONG %1 (%2): %3", _command, _commandName, _data]]]]; + }; + }; + }; + case "COMMAND_INT": { + "armatak" callExtension ["log", [["info", format ["Received MAVLINK COMMAND_INT %1 (%2): %3", _command, _commandName, _data]]]]; + }; + case "COMMAND_ACK": { + "armatak" callExtension ["log", [["info", format ["Received MAVLINK COMMAND_ACK %1 (%2): %3", _command, _commandName, _data]]]]; + }; + case "MANUAL_CONTROL": { + "armatak" callExtension ["log", [["info", format ["Received MAVLINK MANUAL_CONTROL: %1", _data]]]]; + }; + default { + "armatak" callExtension ["log", [["info", format ["Unhandled MAVLINK UDP callback %1: %2", _function, _data]]]]; + }; +}; diff --git a/addons/uav/functions/fnc_parseMavlinkCallbackData.sqf b/addons/uav/functions/fnc_parseMavlinkCallbackData.sqf new file mode 100644 index 0000000..b581623 --- /dev/null +++ b/addons/uav/functions/fnc_parseMavlinkCallbackData.sqf @@ -0,0 +1,17 @@ +#include "..\script_component.hpp" + +params [["_raw", "", [""]]]; + +private _pairs = createHashMap; + +{ + private _entry = _x; + private _separatorIndex = _entry find "="; + if (_separatorIndex > 0) then { + private _key = _entry select [0, _separatorIndex]; + private _value = _entry select [_separatorIndex + 1]; + _pairs set [_key, _value]; + }; +} forEach (_raw splitString ";"); + +_pairs diff --git a/addons/uav/functions/fnc_resolveVideoUri.sqf b/addons/uav/functions/fnc_resolveVideoUri.sqf new file mode 100644 index 0000000..88f0ad3 --- /dev/null +++ b/addons/uav/functions/fnc_resolveVideoUri.sqf @@ -0,0 +1,37 @@ +#include "..\script_component.hpp" + +params [["_uav", objNull, [objNull]]]; + +private _defaultVideoUri = "rtsp://irontak.com:554/fpv"; +private _activelyControlledUav = if (!isNull player) then {getConnectedUAV player} else {objNull}; + +private _normalize = { + params ["_rawUrl"]; + + private _url = trim _rawUrl; + if (_url isEqualTo "") exitWith {""}; + + if (_url find "://" >= 0) exitWith {_url}; + + if (_url find "/" >= 0) exitWith { + format ["rtsp://%1", _url] + }; + + format ["rtp://%1", _url] +}; + +if (!isNull _uav && {_activelyControlledUav isEqualTo _uav}) then { + private _objectVideoUrl = [_uav] call armatak_fnc_extract_marker_video_url; + private _normalizedObjectVideoUrl = [_objectVideoUrl] call _normalize; + if (_normalizedObjectVideoUrl isNotEqualTo "") exitWith { + _normalizedObjectVideoUrl + }; +}; + +private _sessionVideoUrl = player getVariable [QEGVAR(client,video_feed_url), ""]; +private _normalizedSessionVideoUrl = [_sessionVideoUrl] call _normalize; +if (_normalizedSessionVideoUrl isNotEqualTo "") exitWith { + _normalizedSessionVideoUrl +}; + +_defaultVideoUri diff --git a/addons/uav/functions/fnc_updateMavlinkBroadcast.sqf b/addons/uav/functions/fnc_updateMavlinkBroadcast.sqf index 1bc7823..bf3b0c7 100644 --- a/addons/uav/functions/fnc_updateMavlinkBroadcast.sqf +++ b/addons/uav/functions/fnc_updateMavlinkBroadcast.sqf @@ -37,6 +37,9 @@ private _mavlinkAddress = player getVariable [QEGVAR(client,mavlink_address), "" if (_mavlinkAddress isEqualTo "") exitWith {}; private _pos = [_uav] call EFUNC(client,extractClientPosition); +private _uuid = [_uav] call armatak_fnc_extract_uuid; +private _callsign = [_uav] call armatak_fnc_extract_marker_callsign; +private _videoUri = [_uav] call FUNC(resolveVideoUri); private _dir = vectorDir _uav; private _up = vectorUp _uav; private _yaw = getDir _uav; @@ -45,24 +48,14 @@ private _roll = asin (((_up select 0) max -1) min 1); private _relAlt = ((getPosATL _uav) select 2) max 0; private _uavType = if (_uav isKindOf "Plane") then {1} else {[2, 3] select (_uav isKindOf "Helicopter")}; -private _bodyPayload = [ - _mavlinkAddress, - 1, - 1, - _uavType, - _pos select 1, - _pos select 2, - _pos select 3, - _relAlt, - _pos select 5, - _pos select 6, - _roll, - _pitch, - _yaw, - parseNumber isEngineOn _uav -]; - -"armatak" callExtension ["mavlink_mock:send_uas_telemetry", [_bodyPayload]]; +private _gimbalRoll = 0; +private _gimbalPitch = _pitch; +private _gimbalYaw = _yaw; +private _hfov = _uav getVariable ["armatak_uas_fov", 60]; +private _vfov = _uav getVariable ["armatak_uas_vfov", (_hfov * 0.5625)]; +private _imageLat = _pos select 1; +private _imageLon = _pos select 2; +private _imageAlt = _pos select 3; private _laser = laserTarget _uav; if (!isNull _laser) then { @@ -84,27 +77,40 @@ if (!isNull _laser) then { private _camYaw = ((_dirX atan2 _dirY) + 360) mod 360; private _camPitch = _dirZ atan2 (_horizontalMag max 0.001); + _gimbalPitch = _camPitch; + _gimbalYaw = _camYaw; - private _spiAgl = ASLToAGL _spiASL; - private _camRelAlt = (_spiAgl select 2) max 0; - - private _camPayload = [ - _mavlinkAddress, - 2, - 1, - _uavType, - _pos select 1, - _pos select 2, - _pos select 3, - _camRelAlt, - _camYaw, - 0, - 0, - _camPitch, - _camYaw, - 1 - ]; - - "armatak" callExtension ["mavlink_mock:send_uas_telemetry", [_camPayload]]; + private _imageGeo = [_targetWorld select 0, _targetWorld select 1, _targetAslZ] call EFUNC(client,convertClientLocation); + _imageLat = _imageGeo select 0; + _imageLon = _imageGeo select 1; + _imageAlt = _imageGeo select 2; }; }; + +private _systemPayload = [ + _mavlinkAddress, + _uuid, + _callsign, + _uavType, + _pos select 1, + _pos select 2, + _pos select 3, + _relAlt, + _pos select 5, + _pos select 6, + _roll, + _pitch, + _yaw, + parseNumber isEngineOn _uav, + _gimbalRoll, + _gimbalPitch, + _gimbalYaw, + _videoUri, + _hfov, + _vfov, + _imageLat, + _imageLon, + _imageAlt +]; + +"armatak" callExtension ["uas:send_uas_system", [_systemPayload]];