21 Commits

Author SHA1 Message Date
37aa51f8c2 Added handler for scripted stuff to not route all sensor data 2026-05-25 03:06:25 -03:00
ad03444ff5 fixed draw route function data formats 2026-05-25 03:04:28 -03:00
167d47e488 Added scope param to all draw functions 2026-05-24 16:08:59 -03:00
3a82dba854 Addded draw route rust structs 2026-05-24 16:07:23 -03:00
1fff02cdd2 Added Draw Route function 2026-05-24 16:07:04 -03:00
1ab318c279 Added register and delete cot functions to handle CoT delete commands 2026-05-24 16:06:50 -03:00
7a1a4b7372 Fixed leak global vars from convert client pos and extract role functions 2026-05-24 02:58:32 -03:00
2802f9cdc8 Removed a lot of specific mods and added mission folder to ignore 2026-05-15 20:20:52 -03:00
ed039035df Added fallback mTLS enrollment port to handle OTS authentication 2026-05-15 16:33:53 -03:00
7fa4a7c411 Readded serverside digital pointer with a handler to avoid duplicated SPI when a entity is being controlled clientside 2026-05-14 19:05:58 -03:00
d4dfd80cdf Added initial Laser Ranger Finder simulator on port 17211 (as default) emulate digital pointer for in game laser designators 2026-05-14 18:53:34 -03:00
720f9da2df Added Delete CoT handler for removing serverside CoT when a player invokes the clientside connection 2026-05-14 18:52:40 -03:00
5015f09d1d [WIP] Adding Drawing Cursor Over Time functions 2026-05-11 16:26:17 -03:00
882a35c2cd Added report marker functions and stale time param to CoT type 2026-05-11 16:25:52 -03:00
6b3ce96c18 formatted some rust docs 2026-05-11 16:24:40 -03:00
760027b925 reduced readme 2026-05-10 02:07:35 -03:00
9b05bcccc2 Added UAS Tool active actions from ATAK 2026-05-10 01:19:39 -03:00
80320c0f2c added land and home storage 2026-05-10 01:19:13 -03:00
b43a6c9748 added undefined uri 2026-05-10 01:18:11 -03:00
dcc9e1d469 Added handler for turreted drones to share multiple camera components to UAS Tool 2026-05-09 11:58:44 -03:00
3c37185c1a Fixed UAS stream URI resolver 2026-05-09 11:58:08 -03:00
62 changed files with 4009 additions and 308 deletions

3
.gitignore vendored
View File

@@ -89,3 +89,6 @@ target/
local.properties local.properties
*.apk *.apk
.hemtt/missions

View File

@@ -39,30 +39,6 @@ preset = "Hemtt"
workshop = [ workshop = [
"450814997", # CBA_A3 "450814997", # CBA_A3
"463939057", # ace "463939057", # ace
"751965892", # ACRE2
"2522638637", # ACE3 Arsenal Extended - Core
"333310405", # Enhanced Movement
"2034363662", # Enhanced Movement Rework
"843577117", # RHSUSAF
"3147473073", # TOTT Core
"623475643", # 3den Enhanced
"520618345", # Jbad
"1779063631", # Zeus Enhanced
"2397360831", # USAF Mod - Main
"2397376046", # USAF Mod - Utility
"3147476552", # TOTT Optics
"583496184", # CUP Terrains - Core
"2560276469", # Restrict Markers
"1858075458", # LAMBS_Danger.fsm
"3407948300", # JSRS SOUNDMOD 2025
"2834576684", # ITN - Illuminate The Night
"3375788189", # Immersion Cigs - Rewrite
"2095827925", # Brighter Flares
"2257686620", # Blastcore Murr Edition
"2260572637", # BettIR NVG
"3493557838", # Broad Spectrum Warfare
"1291778160", # Hellanmaa
"3533734689", # FPV
] ]
parameters = [ parameters = [

View File

@@ -4,10 +4,6 @@
ARMATAK is a server side Arma 3 addons for streaming unit positions to TAK Clients in sessions on real locations maps. It can be runned both as a clientside mod or a serverside mod, when runned serverside, it will create a TCP Socket connection between Arma 3 and the TAK Server, sending the game session information into it. When used clientside, Arma 3 will host a websocket server that you can connect to your phone and mock the phone's location to the player's in game location. ARMATAK is a server side Arma 3 addons for streaming unit positions to TAK Clients in sessions on real locations maps. It can be runned both as a clientside mod or a serverside mod, when runned serverside, it will create a TCP Socket connection between Arma 3 and the TAK Server, sending the game session information into it. When used clientside, Arma 3 will host a websocket server that you can connect to your phone and mock the phone's location to the player's in game location.
The server-side CoT router supports two transports:
- Plain TCP, for legacy TAK ingress.
- Mutual TLS, using the TAK Server authentication API, so the Arma session can publish as an authenticated TAK device on port `8089`.
## Get in Touch ## Get in Touch
[Join the Discord Server for ARMATAK!](https://discord.gg/svK64PCycU) [Join the Discord Server for ARMATAK!](https://discord.gg/svK64PCycU)

View File

@@ -1,3 +1,4 @@
PREP(convertClientLocation); PREP(convertClientLocation);
PREP(extractClientPosition); PREP(extractClientPosition);
PREP(sendLaserRangeFinder);
PREP(startUDPSocket); PREP(startUDPSocket);

View File

@@ -6,3 +6,4 @@ _local_address = "armatak" callExtension ["local_ip", []] select 0;
SETVAR(player,GVAR(localAddress),_local_address); SETVAR(player,GVAR(localAddress),_local_address);
SETVAR(player,GVAR(eudConnected),false); SETVAR(player,GVAR(eudConnected),false);
SETVAR(player,GVAR(lrfEnabled),false);

View File

@@ -12,7 +12,7 @@ class armatak_udp_socket_start_dialog {
x = "0.386562 * safezoneW + safezoneX"; x = "0.386562 * safezoneW + safezoneX";
y = "0.357 * safezoneH + safezoneY"; y = "0.357 * safezoneH + safezoneY";
w = "0.216563 * safezoneW"; w = "0.216563 * safezoneW";
h = "0.418 * safezoneH"; h = "0.495 * safezoneH";
colorBackground[]={0,0,0,0.45}; colorBackground[]={0,0,0,0.45};
}; };
}; };
@@ -48,12 +48,22 @@ class armatak_udp_socket_start_dialog {
idc = 16969; idc = 16969;
text = ""; text = "";
x = "0.391719 * safezoneW + safezoneX"; x = "0.391719 * safezoneW + safezoneX";
y = "0.632 * safezoneH + safezoneY"; y = "0.709 * safezoneH + safezoneY";
w = "0.20625 * safezoneW"; w = "0.20625 * safezoneW";
h = "0.044 * safezoneH"; h = "0.044 * safezoneH";
colorBackground[]={0,0,0,0.5}; 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."; 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_lrf_port_edit: RscEdit {
idc = 16971;
text = "17211";
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 = "ATAK local Laser Range Finder UDP input. Leave empty to disable.";
};
class armatak_gui_module_udp_socket_dialog_address_text: RscText { class armatak_gui_module_udp_socket_dialog_address_text: RscText {
idc = 16963; idc = 16963;
text = "EUD's Address"; text = "EUD's Address";
@@ -82,6 +92,14 @@ class armatak_udp_socket_start_dialog {
idc = 16970; idc = 16970;
text = "Video Feed URL (Optional)"; text = "Video Feed URL (Optional)";
x = "0.391719 * safezoneW + safezoneX"; x = "0.391719 * safezoneW + safezoneX";
y = "0.676 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.033 * safezoneH";
};
class armatak_gui_module_udp_socket_dialog_lrf_port_text: RscText {
idc = 16972;
text = "Laser Range Finder Port";
x = "0.391719 * safezoneW + safezoneX";
y = "0.599 * safezoneH + safezoneY"; y = "0.599 * safezoneH + safezoneY";
w = "0.20625 * safezoneW"; w = "0.20625 * safezoneW";
h = "0.033 * safezoneH"; h = "0.033 * safezoneH";
@@ -91,7 +109,7 @@ class armatak_udp_socket_start_dialog {
text = "Cancel"; text = "Cancel";
action = "closeDialog 2;"; action = "closeDialog 2;";
x = "0.551563 * safezoneW + safezoneX"; x = "0.551563 * safezoneW + safezoneX";
y = "0.709 * safezoneH + safezoneY"; y = "0.786 * safezoneH + safezoneY";
w = "0.0464063 * safezoneW"; w = "0.0464063 * safezoneW";
h = "0.055 * safezoneH"; h = "0.055 * safezoneH";
}; };
@@ -100,7 +118,7 @@ class armatak_udp_socket_start_dialog {
text = "Ok"; text = "Ok";
action = QUOTE(call FUNC(startUDPSocket)); action = QUOTE(call FUNC(startUDPSocket));
x = "0.5 * safezoneW + safezoneX"; x = "0.5 * safezoneW + safezoneX";
y = "0.709 * safezoneH + safezoneY"; y = "0.786 * safezoneH + safezoneY";
w = "0.0464063 * safezoneW"; w = "0.0464063 * safezoneW";
h = "0.055 * safezoneH"; h = "0.055 * safezoneH";
}; };

View File

@@ -19,11 +19,15 @@
* Public: Yes * Public: Yes
*/ */
params["_latitude", "_longitude", "_altitude"]; params [
["_latitude", 0, [0]],
["_longitude", 0, [0]],
["_altitude", 0, [0]]
];
_position = [_latitude, _longitude, _altitude]; private _position = [_latitude, _longitude, _altitude];
_realLocation = null; private _realLocation = [0, 0, _altitude];
switch (toLower worldName) do { switch (toLower worldName) do {
case "altis": { case "altis": {

View File

@@ -0,0 +1,52 @@
#include "..\script_component.hpp"
params ["_unit"];
private _lrfEnabled = player getVariable [QGVAR(lrfEnabled), false];
private _uid = format ["%1.LRF", _unit call armatak_fnc_extract_uuid];
private _laserTarget = laserTarget _unit;
if (!isNull _laserTarget) exitWith {
private _originASL = getPosASL _unit;
private _targetASL = getPosASL _laserTarget;
private _delta = _targetASL vectorDiff _originASL;
private _dx = _delta select 0;
private _dy = _delta select 1;
private _dz = _delta select 2;
private _horizontalDistance = sqrt ((_dx * _dx) + (_dy * _dy));
private _slantDistance = (_originASL vectorDistance _targetASL) max 1;
private _azimuth = (((_dx atan2 _dy) + 360) mod 360);
private _elevation = _dz atan2 (_horizontalDistance max 0.001);
private _lastTargetASL = player getVariable [QGVAR(lrfLastTargetASL), []];
private _lastSentAt = player getVariable [QGVAR(lrfLastSentAt), -1000];
private _targetMoved = _lastTargetASL isEqualTo [] || {(_lastTargetASL vectorDistance _targetASL) > 5};
private _sendCooldownElapsed = (time - _lastSentAt) >= 2.5;
player setVariable [QGVAR(lrfWasActive), true];
player setVariable [QGVAR(lrfLostAt), -1];
player setVariable [QGVAR(lrfClearSent), false];
if (_lrfEnabled && {_targetMoved} && {_sendCooldownElapsed}) then {
"armatak" callExtension ["udp_socket:send_lrf", [[_uid, _slantDistance, _azimuth, _elevation]]];
player setVariable [QGVAR(lrfLastTargetASL), _targetASL];
player setVariable [QGVAR(lrfLastSentAt), time];
};
};
if !(player getVariable [QGVAR(lrfWasActive), false]) exitWith {};
private _lostAt = player getVariable [QGVAR(lrfLostAt), -1];
if (_lostAt < 0) then {
player setVariable [QGVAR(lrfLostAt), time];
};
private _clearSent = player getVariable [QGVAR(lrfClearSent), false];
if (_lrfEnabled && {!_clearSent} && {(time - (player getVariable [QGVAR(lrfLostAt), time])) >= 6}) then {
"armatak" callExtension ["udp_socket:clear_lrf", [_uid]];
player setVariable [QGVAR(lrfWasActive), false];
player setVariable [QGVAR(lrfClearSent), true];
player setVariable [QGVAR(lrfLastTargetASL), []];
player setVariable [QGVAR(lrfLastSentAt), -1000];
};

View File

@@ -14,24 +14,38 @@ disableSerialization;
private _eud_address = ctrlText 16961; private _eud_address = ctrlText 16961;
private _gnss_port = ctrlText 16962; private _gnss_port = ctrlText 16962;
private _mavlink_port = ctrlText 16967; private _mavlink_port = ctrlText 16967;
private _lrf_port = ctrlText 16971;
private _video_feed_url = ctrlText 16969; private _video_feed_url = ctrlText 16969;
private _udp_socket_fulladdress = _eud_address + ":" + _gnss_port; private _udp_socket_fulladdress = _eud_address + ":" + _gnss_port;
private _mavlink_address = _eud_address + ":" + _mavlink_port; private _mavlink_address = _eud_address + ":" + _mavlink_port;
private _lrf_port_trimmed = trim _lrf_port;
private _lrf_enabled = _lrf_port_trimmed isNotEqualTo "";
private _lrf_address = _eud_address + ":" + _lrf_port_trimmed;
player setVariable [QGVAR(udp_socket_address), _udp_socket_fulladdress]; player setVariable [QGVAR(udp_socket_address), _udp_socket_fulladdress];
player setVariable [QGVAR(mavlink_address), _mavlink_address]; player setVariable [QGVAR(mavlink_address), _mavlink_address];
player setVariable [QGVAR(lrf_address), _lrf_address];
player setVariable [QGVAR(lrfEnabled), _lrf_enabled];
player setVariable [QGVAR(lrfWasActive), false];
player setVariable [QGVAR(lrfLostAt), -1];
player setVariable [QGVAR(lrfClearSent), false];
player setVariable [QGVAR(lrfLastTargetASL), []];
player setVariable [QGVAR(lrfLastSentAt), -1000];
player setVariable [QGVAR(video_feed_url), trim _video_feed_url]; player setVariable [QGVAR(video_feed_url), trim _video_feed_url];
player setVariable [QGVAR(eudConnected), true]; player setVariable [QGVAR(eudConnected), true, true];
private _advertised_video_uri = [objNull] call EFUNC(uav,resolveVideoUri); private _advertised_video_uri = [objNull] call EFUNC(uav,resolveVideoUri);
"armatak" callExtension ["udp_socket:start", [_udp_socket_fulladdress]]; "armatak" callExtension ["udp_socket:start", [_udp_socket_fulladdress]];
"armatak" callExtension ["uas:start_endpoint", [parseNumber _mavlink_port]]; "armatak" callExtension ["uas:start_endpoint", [parseNumber _mavlink_port]];
if (_lrf_enabled) then {
"armatak" callExtension ["udp_socket:start_lrf", [_lrf_address]];
};
private _mdnsInstanceName = format ["ArmaTAK-%1", name player]; private _mdnsInstanceName = format ["ArmaTAK-%1", name player];
"armatak" callExtension ["mdns:start_uas_advertisement", [_mdnsInstanceName, parseNumber _mavlink_port, _advertised_video_uri]]; "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]]]]; "armatak" callExtension ["log", [["info", format ["Client UDP socket started for %1, MAVLink target set to %2, LRF target set to %3 and advertised video URI set to %4. Digital pointer uses ATAK LRF when enabled.", _udp_socket_fulladdress, _mavlink_address, _lrf_address, _advertised_video_uri]]]];
call EFUNC(uav,startMavlinkBroadcast); call EFUNC(uav,startMavlinkBroadcast);
@@ -41,5 +55,11 @@ call EFUNC(uav,startMavlinkBroadcast);
"armatak" callExtension ["udp_socket:send_gps_cot", [player call FUNC(extractClientPosition)]]; "armatak" callExtension ["udp_socket:send_gps_cot", [player call FUNC(extractClientPosition)]];
}, 0.5, []] call CBA_fnc_addPerFrameHandler; }, 0.5, []] call CBA_fnc_addPerFrameHandler;
[{
if !(player getVariable [QGVAR(eudConnected), false]) exitWith {};
[player] call FUNC(sendLaserRangeFinder);
}, 0.25, []] call CBA_fnc_addPerFrameHandler;
deleteVehicle _logic; deleteVehicle _logic;
closeDialog 1; closeDialog 1;

View File

@@ -19,6 +19,33 @@ 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 report_marker {
file = "\armatak\armatak\addons\main\functions\api\fn_report_marker.sqf";
};
class register_cot {
file = "\armatak\armatak\addons\main\functions\api\fn_register_cot.sqf";
};
class delete_registered_cots {
file = "\armatak\armatak\addons\main\functions\api\fn_delete_registered_cots.sqf";
};
class draw_circle {
file = "\armatak\armatak\addons\main\functions\api\fn_draw_circle.sqf";
};
class draw_ellipse {
file = "\armatak\armatak\addons\main\functions\api\fn_draw_ellipse.sqf";
};
class draw_rectangle {
file = "\armatak\armatak\addons\main\functions\api\fn_draw_rectangle.sqf";
};
class draw_polyline {
file = "\armatak\armatak\addons\main\functions\api\fn_draw_polyline.sqf";
};
class draw_route {
file = "\armatak\armatak\addons\main\functions\api\fn_draw_route.sqf";
};
class draw_tactical_graphic {
file = "\armatak\armatak\addons\main\functions\api\fn_draw_tactical_graphic.sqf";
};
class send_uas_platform_cot { class send_uas_platform_cot {
file = "\armatak\armatak\addons\main\functions\api\fn_send_uas_platform_cot.sqf"; file = "\armatak\armatak\addons\main\functions\api\fn_send_uas_platform_cot.sqf";
}; };
@@ -146,4 +173,3 @@ class CfgFunctions {
}; };
}; };
}; };

View File

@@ -9,10 +9,11 @@ addMissionEventHandler ["ExtensionCallback", {
switch (_function) do { switch (_function) do {
case "EUD Connected": { case "EUD Connected": {
SETVAR(player,EGVAR(client,eudConnected),true); player setVariable [QEGVAR(client,eudConnected), true, true];
}; };
case "EUD Disconnected": { case "EUD Disconnected": {
SETVAR(player,EGVAR(client,eudConnected),false); player setVariable [QEGVAR(client,eudConnected), false, true];
SETVAR(player,EGVAR(client,lrfEnabled),false);
call EFUNC(uav,stopMavlinkBroadcast); call EFUNC(uav,stopMavlinkBroadcast);
"armatak" callExtension ["uas:stop_endpoint", []]; "armatak" callExtension ["uas:stop_endpoint", []];
"armatak" callExtension ["mdns:stop", []]; "armatak" callExtension ["mdns:stop", []];
@@ -27,14 +28,16 @@ addMissionEventHandler ["ExtensionCallback", {
[_function, "error", _name] call FUNC(notify); [_function, "error", _name] call FUNC(notify);
if (_function == "UDP Socket is not running") then { if (_function == "UDP Socket is not running") then {
SETVAR(player,EGVAR(client,eudConnected),false); player setVariable [QEGVAR(client,eudConnected), false, true];
SETVAR(player,EGVAR(client,lrfEnabled),false);
call EFUNC(uav,stopMavlinkBroadcast); call EFUNC(uav,stopMavlinkBroadcast);
"armatak" callExtension ["uas:stop_endpoint", []]; "armatak" callExtension ["uas:stop_endpoint", []];
"armatak" callExtension ["mdns:stop", []]; "armatak" callExtension ["mdns:stop", []];
}; };
if (_function == "failed to bind UDP socket") then { if (_function == "failed to bind UDP socket") then {
SETVAR(player,EGVAR(client,eudConnected),false); player setVariable [QEGVAR(client,eudConnected), false, true];
SETVAR(player,EGVAR(client,lrfEnabled),false);
call EFUNC(uav,stopMavlinkBroadcast); call EFUNC(uav,stopMavlinkBroadcast);
"armatak" callExtension ["uas:stop_endpoint", []]; "armatak" callExtension ["uas:stop_endpoint", []];
"armatak" callExtension ["mdns:stop", []]; "armatak" callExtension ["mdns:stop", []];
@@ -61,6 +64,14 @@ addMissionEventHandler ["ExtensionCallback", {
case "COMMAND_LONG"; case "COMMAND_LONG";
case "COMMAND_INT"; case "COMMAND_INT";
case "COMMAND_ACK"; case "COMMAND_ACK";
case "MISSION_COUNT";
case "MISSION_ITEM";
case "MISSION_ITEM_INT";
case "MISSION_CLEAR_ALL";
case "MISSION_SET_CURRENT";
case "SET_HOME_POSITION";
case "SET_MODE";
case "SET_POSITION_TARGET_GLOBAL_INT";
case "MANUAL_CONTROL": { case "MANUAL_CONTROL": {
"armatak" callExtension ["log", [["info", format ["MAVLINK UDP CALLBACK %1 %2", _function, _data]]]]; "armatak" callExtension ["log", [["info", format ["MAVLINK UDP CALLBACK %1 %2", _function, _data]]]];
[_function, _data] call EFUNC(uav,handleMavlinkCallback); [_function, _data] call EFUNC(uav,handleMavlinkCallback);

View File

@@ -0,0 +1,36 @@
// function name: armatak_fnc_delete_registered_cots
// function author: Valmo, Codex
// function description: Sends forced delete CoTs for all registered CoTs in a scope.
//
// Arguments:
// 0: Scope/key used to group CoTs <STRING>
//
// Return Value:
// Number of delete CoTs sent <NUMBER>
//
// Public: Yes
params [
["_scope", "", [""]]
];
if (_scope isEqualTo "") exitWith {0};
private _registry = missionNamespace getVariable ["armatak_registered_cots", []];
private _remaining = [];
private _deleted = 0;
{
_x params ["_registeredScope", "_uid", "_type", "_lat", "_lon", "_hae"];
if (_registeredScope isEqualTo _scope) then {
"armatak" callExtension ["tcp_socket:cot:delete", [[_uid, _type, _lat, _lon, _hae]]];
_deleted = _deleted + 1;
} else {
_remaining pushBack _x;
};
} forEach _registry;
missionNamespace setVariable ["armatak_registered_cots", _remaining];
_deleted

View File

@@ -0,0 +1,44 @@
// function name: armatak_fnc_draw_circle
// function author: Valmo
// function description: Sends an ATAK Drawing Tools circle CoT.
//
// Arguments:
// 0: Center position or object <ARRAY|OBJECT>
// 1: Radius in meters <NUMBER>
// 2: Callsign/title <STRING> (default: "ArmaTAK Circle")
// 3: Stale time in seconds <NUMBER> (default: 86400)
// 4: Stroke color as signed ARGB int <NUMBER> (default: -1)
// 5: Fill color as signed ARGB int <NUMBER> (default: -1761607681)
// 6: Stroke weight <NUMBER> (default: 3)
// 7: Optional registration scope <STRING> (default: "")
//
// Example:
// [player, 300, "Mortar Risk Area"] call armatak_fnc_draw_circle;
//
// Public: Yes
params [
["_center", objNull, [objNull, []]],
["_radius", 100, [0]],
["_callsign", "ArmaTAK Circle", [""]],
["_staleSeconds", 86400, [0]],
["_strokeColor", -1, [0]],
["_fillColor", -1761607681, [0]],
["_strokeWeight", 3, [0]],
["_scope", "", [""]]
];
[
_center,
_radius,
_radius,
360,
_callsign,
_staleSeconds,
_strokeColor,
_fillColor,
_strokeWeight,
"",
"u-d-c-c",
_scope
] call armatak_fnc_draw_ellipse;

View File

@@ -0,0 +1,70 @@
// function name: armatak_fnc_draw_ellipse
// function author: Valmo
// function description: Sends an ATAK Drawing Tools ellipse or circle CoT.
//
// Arguments:
// 0: Center position or object <ARRAY|OBJECT>
// 1: Major radius in meters <NUMBER>
// 2: Minor radius in meters <NUMBER>
// 3: Rotation angle in degrees <NUMBER> (default: 0)
// 4: Callsign/title <STRING> (default: "ArmaTAK Ellipse")
// 5: Stale time in seconds <NUMBER> (default: 86400)
// 6: Stroke color as signed ARGB int <NUMBER> (default: -1)
// 7: Fill color as signed ARGB int <NUMBER> (default: -1761607681)
// 8: Stroke weight <NUMBER> (default: 3)
// 9: MilSym SIDC for tactical overlay <STRING> (default: "")
// 10: CoT type <STRING> (default: "u-d-c-e")
// 11: Optional registration scope <STRING> (default: "")
//
// Example:
// [screenToWorld [0.5, 0.5], 250, 100, 45, "Support by Fire"] call armatak_fnc_draw_ellipse;
//
// Public: Yes
params [
["_center", objNull, [objNull, []]],
["_major", 100, [0]],
["_minor", 50, [0]],
["_angle", 0, [0]],
["_callsign", "ArmaTAK Ellipse", [""]],
["_staleSeconds", 86400, [0]],
["_strokeColor", -1, [0]],
["_fillColor", -1761607681, [0]],
["_strokeWeight", 3, [0]],
["_milsym", "", [""]],
["_cotType", "u-d-c-e", [""]],
["_scope", "", [""]]
];
private _position = if (_center isEqualType objNull) then {
getPos _center
} else {
_center
};
if ((count _position) < 2) exitWith {""};
private _altitude = _position param [2, 0, [0]];
private _realLocation = [_position select 0, _position select 1, _altitude] call armatak_client_fnc_convertClientLocation;
private _uuid = "armatak" callExtension ["uuid", []] select 0;
private _payload = [
_uuid,
_cotType,
_realLocation select 0,
_realLocation select 1,
_realLocation select 2,
_major max 1,
_minor max 1,
_angle,
_callsign,
_staleSeconds max 1,
_strokeColor,
_fillColor,
_strokeWeight max 1,
_milsym
];
"armatak" callExtension ["tcp_socket:draw:ellipse", [_payload]];
[_scope, _uuid, _cotType, _realLocation select 0, _realLocation select 1, _realLocation select 2] call armatak_fnc_register_cot;
_uuid

View File

@@ -0,0 +1,83 @@
// function name: armatak_fnc_draw_polyline
// function author: Valmo
// function description: Sends an ATAK Drawing Tools freeform line or polygon CoT.
//
// Arguments:
// 0: Positions or objects <ARRAY>
// 1: Callsign/title <STRING> (default: "ArmaTAK Line")
// 2: Closed polygon <BOOL> (default: false)
// 3: Stale time in seconds <NUMBER> (default: 86400)
// 4: Stroke color as signed ARGB int <NUMBER> (default: -1)
// 5: Fill color as signed ARGB int <NUMBER> (default: -1761607681)
// 6: Stroke weight <NUMBER> (default: 3)
// 7: Stroke style <STRING> (default: "solid")
// 8: MilSym SIDC for tactical overlay <STRING> (default: "")
// 9: CoT type <STRING> (default: "u-d-f")
// 10: Optional registration scope <STRING> (default: "")
//
// Example:
// [[pos player, screenToWorld [0.5, 0.5]], "Phase Line Blue"] call armatak_fnc_draw_polyline;
//
// Public: Yes
params [
["_points", [], [[]]],
["_callsign", "ArmaTAK Line", [""]],
["_closed", false, [true]],
["_staleSeconds", 86400, [0]],
["_strokeColor", -1, [0]],
["_fillColor", -1761607681, [0]],
["_strokeWeight", 3, [0]],
["_strokeStyle", "solid", [""]],
["_milsym", "", [""]],
["_cotType", "u-d-f", [""]],
["_scope", "", [""]]
];
if ((count _points) < 2) exitWith {""};
private _pointStrings = [];
private _center = [];
{
private _position = if (_x isEqualType objNull) then {
getPos _x
} else {
_x
};
if ((count _position) >= 2) then {
private _altitude = _position param [2, 0, [0]];
private _realLocation = [_position select 0, _position select 1, _altitude] call armatak_client_fnc_convertClientLocation;
_pointStrings pushBack format ["%1,%2,%3", _realLocation select 0, _realLocation select 1, _realLocation select 2];
if (_center isEqualTo []) then {
_center = _realLocation;
};
};
} forEach _points;
if ((count _pointStrings) < 2) exitWith {""};
private _uuid = "armatak" callExtension ["uuid", []] select 0;
private _payload = [
_uuid,
_cotType,
_center select 0,
_center select 1,
_center select 2,
_pointStrings joinString ";",
_callsign,
_staleSeconds max 1,
_strokeColor,
_fillColor,
_strokeWeight max 1,
_strokeStyle,
_closed,
_milsym
];
"armatak" callExtension ["tcp_socket:draw:free", [_payload]];
[_scope, _uuid, _cotType, _center select 0, _center select 1, _center select 2] call armatak_fnc_register_cot;
_uuid

View File

@@ -0,0 +1,94 @@
// function name: armatak_fnc_draw_rectangle
// function author: Valmo
// function description: Sends an ATAK Drawing Tools rectangle CoT from an Arma center, width, length, and bearing.
//
// Arguments:
// 0: Center position or object <ARRAY|OBJECT>
// 1: Width in meters <NUMBER>
// 2: Length in meters <NUMBER>
// 3: Bearing in degrees <NUMBER> (default: 0)
// 4: Callsign/title <STRING> (default: "ArmaTAK Rectangle")
// 5: Stale time in seconds <NUMBER> (default: 86400)
// 6: Stroke color as signed ARGB int <NUMBER> (default: -1)
// 7: Fill color as signed ARGB int <NUMBER> (default: -1761607681)
// 8: Stroke weight <NUMBER> (default: 3)
// 9: MilSym SIDC for tactical overlay <STRING> (default: "")
// 10: Optional registration scope <STRING> (default: "")
//
// Example:
// [screenToWorld [0.5, 0.5], 200, 500, 30, "Engagement Area"] call armatak_fnc_draw_rectangle;
//
// Public: Yes
params [
["_center", objNull, [objNull, []]],
["_width", 100, [0]],
["_length", 100, [0]],
["_bearing", 0, [0]],
["_callsign", "ArmaTAK Rectangle", [""]],
["_staleSeconds", 86400, [0]],
["_strokeColor", -1, [0]],
["_fillColor", -1761607681, [0]],
["_strokeWeight", 3, [0]],
["_milsym", "", [""]],
["_scope", "", [""]]
];
private _centerPos = if (_center isEqualType objNull) then {
getPos _center
} else {
_center
};
if ((count _centerPos) < 2) exitWith {""};
private _altitude = _centerPos param [2, 0, [0]];
private _halfWidth = (_width max 1) / 2;
private _halfLength = (_length max 1) / 2;
private _sin = sin _bearing;
private _cos = cos _bearing;
private _forward = [_sin, _cos, 0];
private _right = [_cos, -_sin, 0];
private _offsets = [
[(_right vectorMultiply -_halfWidth), (_forward vectorMultiply _halfLength)],
[(_right vectorMultiply _halfWidth), (_forward vectorMultiply _halfLength)],
[(_right vectorMultiply _halfWidth), (_forward vectorMultiply -_halfLength)],
[(_right vectorMultiply -_halfWidth), (_forward vectorMultiply -_halfLength)]
];
private _points = [];
{
private _offset = (_x select 0) vectorAdd (_x select 1);
_points pushBack ([_centerPos select 0, _centerPos select 1, _altitude] vectorAdd _offset);
} forEach _offsets;
private _centerReal = [_centerPos select 0, _centerPos select 1, _altitude] call armatak_client_fnc_convertClientLocation;
private _pointStrings = [];
{
private _realLocation = [_x select 0, _x select 1, _x select 2] call armatak_client_fnc_convertClientLocation;
_pointStrings pushBack format ["%1,%2,%3", _realLocation select 0, _realLocation select 1, _realLocation select 2];
} forEach _points;
private _uuid = "armatak" callExtension ["uuid", []] select 0;
private _payload = [
_uuid,
"u-d-r",
_centerReal select 0,
_centerReal select 1,
_centerReal select 2,
_pointStrings joinString ";",
_callsign,
_staleSeconds max 1,
_strokeColor,
_fillColor,
_strokeWeight max 1,
"solid",
false,
_milsym
];
"armatak" callExtension ["tcp_socket:draw:rectangle", [_payload]];
[_scope, _uuid, "u-d-r", _centerReal select 0, _centerReal select 1, _centerReal select 2] call armatak_fnc_register_cot;
_uuid

View File

@@ -0,0 +1,200 @@
// function name: armatak_fnc_draw_route
// function author: Valmo
// function description: Sends an ATAK navigable route CoT.
//
// Arguments:
// 0: Positions or objects <ARRAY>
// 1: Callsign/title <STRING> (default: "ArmaTAK Route")
// 2: Stale time in seconds <NUMBER> (default: 86400)
// 3: Color as signed ARGB int <NUMBER> (default: -1)
// 4: Stroke weight <NUMBER> (default: 3)
// 5: Navigation method <STRING> (default: "Driving")
// 6: Route type <STRING> (default: "Primary")
// 7: Direction/planning method <STRING> (default: "Infil")
// 8: Checkpoint interval among route points <NUMBER> (default: 5)
// 9: Optional registration scope <STRING> (default: "")
//
// Example:
// [[pos player, screenToWorld [0.5, 0.5]], "Patrol Route"] call armatak_fnc_draw_route;
//
// Public: Yes
params [
["_points", [], [[]]],
["_callsign", "ArmaTAK Route", [""]],
["_staleSeconds", 86400, [0]],
["_color", -1, [0]],
["_strokeWeight", 3, [0]],
["_method", "Driving", [""]],
["_routeType", "Primary", [""]],
["_direction", "Infil", [""]],
["_checkpointInterval", 5, [0]],
["_scope", "", [""]]
];
if ((count _points) < 2) exitWith {""};
private _pointStrings = [];
{
private _position = if (_x isEqualType objNull) then {
getPos _x
} else {
_x
};
if ((count _position) >= 2) then {
private _altitude = _position param [2, 0, [0]];
private _realLocation = [_position select 0, _position select 1, _altitude] call armatak_client_fnc_convertClientLocation;
_pointStrings pushBack format ["%1,%2,%3", _realLocation select 0, _realLocation select 1, _realLocation select 2];
};
} forEach _points;
if ((count _pointStrings) < 2) exitWith {""};
private _uuid = "armatak" callExtension ["uuid", []] select 0;
private _pad = {
params ["_value", "_digits"];
private _text = str (floor _value);
while {(count _text) < _digits} do {
_text = "0" + _text;
};
_text
};
private _formatCotTime = {
params ["_dateParts"];
format [
"%1-%2-%3T%4:%5:%6.%7Z",
_dateParts param [0, 1970, [0]],
[_dateParts param [1, 1, [0]], 2] call _pad,
[_dateParts param [2, 1, [0]], 2] call _pad,
[_dateParts param [3, 0, [0]], 2] call _pad,
[_dateParts param [4, 0, [0]], 2] call _pad,
[_dateParts param [5, 0, [0]], 2] call _pad,
[_dateParts param [6, 0, [0]], 3] call _pad
]
};
private _addSecondsUtc = {
params ["_dateParts", "_secondsToAdd"];
private _year = _dateParts param [0, 1970, [0]];
private _month = _dateParts param [1, 1, [0]];
private _day = _dateParts param [2, 1, [0]];
private _hour = _dateParts param [3, 0, [0]];
private _minute = _dateParts param [4, 0, [0]];
private _second = (_dateParts param [5, 0, [0]]) + (floor _secondsToAdd);
private _millisecond = _dateParts param [6, 0, [0]];
while {_second >= 60} do {
_second = _second - 60;
_minute = _minute + 1;
};
while {_minute >= 60} do {
_minute = _minute - 60;
_hour = _hour + 1;
};
while {_hour >= 24} do {
_hour = _hour - 24;
_day = _day + 1;
private _daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
private _isLeapYear = ((_year mod 4) isEqualTo 0) && {((_year mod 100) isNotEqualTo 0) || {(_year mod 400) isEqualTo 0}};
if (_isLeapYear) then {_daysInMonth set [1, 29];};
if (_day > (_daysInMonth select (_month - 1))) then {
_day = 1;
_month = _month + 1;
if (_month > 12) then {
_month = 1;
_year = _year + 1;
};
};
};
[_year, _month, _day, _hour, _minute, _second, _millisecond]
};
private _nowDate = systemTimeUTC;
private _now = [_nowDate] call _formatCotTime;
private _stale = [[_nowDate, _staleSeconds max 60] call _addSecondsUtc] call _formatCotTime;
private _links = "";
private _routeWaypoints = [];
private _lastIndex = (count _pointStrings) - 1;
private _checkpointNumber = 1;
private _checkpointIntervalSafe = _checkpointInterval max 1;
{
private _linkUuid = "armatak" callExtension ["uuid", []] select 0;
private _pointCallsign = "";
if (_forEachIndex isEqualTo 0) then {
_pointCallsign = format ["%1 SP", _callsign];
} else {
if (_forEachIndex isEqualTo _lastIndex) then {
_pointCallsign = "VDO";
} else {
if ((_forEachIndex mod _checkpointIntervalSafe) isEqualTo 0) then {
_pointCallsign = format ["CP%1", _checkpointNumber];
_checkpointNumber = _checkpointNumber + 1;
};
};
};
private _linkType = ["b-m-p-w", "b-m-p-c"] select (_pointCallsign isEqualTo "");
_links = _links + format [
"<link uid=""%1"" callsign=""%2"" type=""%3"" point=""%4"" remarks="""" relation=""c""/>",
_linkUuid,
_pointCallsign,
_linkType,
_x
];
if (_pointCallsign isNotEqualTo "") then {
_routeWaypoints pushBack [_linkUuid, _pointCallsign, _x];
};
} forEach _pointStrings;
private _xml = format [
"<?xml version=""1.0"" encoding=""UTF-8"" ?><event version=""2.0"" uid=""%1"" type=""b-m-r"" time=""%2"" start=""%2"" stale=""%3"" how=""h-e"" access=""Undefined""><point lat=""0.0"" lon=""0.0"" hae=""9999999.0"" ce=""9999999.0"" le=""9999999.0""/><detail>%4<link_attr planningmethod=""%5"" color=""%6"" method=""%7"" prefix=""CP"" style=""0"" type=""Vehicle"" stroke=""%8"" direction=""%5"" routetype=""%9"" order=""Ascending Check Points""/><creator uid=""ARMATAK"" callsign=""ArmaTAK"" time=""%2"" type=""a-f-G-U-C""/><strokeColor value=""%6""/><strokeWeight value=""%8""/><strokeStyle value=""solid""/><labels_on value=""false""/><__routeinfo><__navcues/></__routeinfo><color value=""%6""/><remarks/><contact callsign=""%10""/><archive/><height_unit>1</height_unit></detail></event>",
_uuid,
_now,
_stale,
_links,
_direction,
_color,
_method,
_strokeWeight max 1,
_routeType,
_callsign
];
"armatak" callExtension ["log", [["info", format ["Sending ATAK route '%1' with %2 points (%3 bytes)", _callsign, count _pointStrings, count _xml]]]];
"armatak" callExtension ["tcp_socket:send_payload", [_xml]];
[_scope, _uuid, "b-m-r", 0, 0, 9999999] call armatak_fnc_register_cot;
{
_x params ["_waypointUid", "_waypointCallsign", "_pointString"];
private _pointParts = _pointString splitString ",";
if ((count _pointParts) >= 3) then {
private _waypointXml = format [
"<?xml version=""1.0"" encoding=""UTF-8"" ?><event version=""2.0"" uid=""%1"" type=""b-m-p-w"" time=""%2"" start=""%2"" stale=""%3"" how=""h-e""><point lat=""%4"" lon=""%5"" hae=""%6"" ce=""9999999.0"" le=""9999999.0""/><detail><contact callsign=""%7""/><remarks/><archive/><link relation=""p-p"" type=""b-m-r"" uid=""%8""/></detail></event>",
_waypointUid,
_now,
_stale,
_pointParts select 0,
_pointParts select 1,
_pointParts select 2,
_waypointCallsign,
_uuid
];
"armatak" callExtension ["tcp_socket:send_payload", [_waypointXml]];
[_scope, _waypointUid, "b-m-p-w", parseNumber (_pointParts select 0), parseNumber (_pointParts select 1), parseNumber (_pointParts select 2)] call armatak_fnc_register_cot;
};
} forEach _routeWaypoints;
_uuid

View File

@@ -0,0 +1,75 @@
// function name: armatak_fnc_draw_tactical_graphic
// function author: Valmo
// function description: Sends an ATAK Drawing Tools shape with a MilSym tactical graphic overlay.
//
// Arguments:
// 0: Positions or objects <ARRAY>
// 1: MilSym SIDC <STRING>
// 2: Callsign/title <STRING> (default: "ArmaTAK Tactical Graphic")
// 3: Closed polygon <BOOL> (default: false)
// 4: Stale time in seconds <NUMBER> (default: 86400)
// 5: Stroke color as signed ARGB int <NUMBER> (default: -1)
// 6: Fill color as signed ARGB int <NUMBER> (default: -1761607681)
// 7: Stroke weight <NUMBER> (default: 3)
//
// Example:
// [[pos player, screenToWorld [0.5, 0.5]], "GFGPOLAGM-----X", "Axis of Advance"] call armatak_fnc_draw_tactical_graphic;
//
// Public: Yes
params [
["_points", [], [[]]],
["_milsym", "", [""]],
["_callsign", "ArmaTAK Tactical Graphic", [""]],
["_closed", false, [true]],
["_staleSeconds", 86400, [0]],
["_strokeColor", -1, [0]],
["_fillColor", -1761607681, [0]],
["_strokeWeight", 3, [0]]
];
if (_milsym isEqualTo "") exitWith {""};
if ((count _points) < 2) exitWith {""};
private _pointStrings = [];
private _center = [];
{
private _position = if (_x isEqualType objNull) then {
getPos _x
} else {
_x
};
if ((count _position) >= 2) then {
private _altitude = _position param [2, 0, [0]];
private _realLocation = [_position select 0, _position select 1, _altitude] call armatak_client_fnc_convertClientLocation;
_pointStrings pushBack format ["%1,%2,%3", _realLocation select 0, _realLocation select 1, _realLocation select 2];
if (_center isEqualTo []) then {
_center = _realLocation;
};
};
} forEach _points;
if ((count _pointStrings) < 2) exitWith {""};
private _uuid = "armatak" callExtension ["uuid", []] select 0;
private _payload = [
_uuid,
"u-d-f",
_center select 0,
_center select 1,
_center select 2,
_pointStrings joinString ";",
_callsign,
_staleSeconds max 1,
_strokeColor,
_fillColor,
_strokeWeight max 1,
"solid",
_closed,
_milsym
];
"armatak" callExtension ["tcp_socket:draw:vector", [_payload]];

View File

@@ -0,0 +1,30 @@
// function name: armatak_fnc_register_cot
// function author: Valmo, Codex
// function description: Registers a CoT object under a scope so it can be deleted later.
//
// Arguments:
// 0: Scope/key used to group CoTs <STRING>
// 1: CoT UID <STRING>
// 2: CoT type <STRING>
// 3: Latitude <NUMBER>
// 4: Longitude <NUMBER>
// 5: HAE altitude <NUMBER>
//
// Public: Yes
params [
["_scope", "", [""]],
["_uid", "", [""]],
["_type", "", [""]],
["_lat", 0, [0]],
["_lon", 0, [0]],
["_hae", 0, [0]]
];
if (_scope isEqualTo "" || {_uid isEqualTo ""} || {_type isEqualTo ""}) exitWith {false};
private _registry = missionNamespace getVariable ["armatak_registered_cots", []];
_registry pushBack [_scope, _uid, _type, _lat, _lon, _hae];
missionNamespace setVariable ["armatak_registered_cots", _registry];
true

View File

@@ -0,0 +1,105 @@
// function name: armatak_fnc_report_marker
// function author: Valmo
// function description: Sends a one-shot TAK report marker with an independent stale time.
//
// Arguments:
// 0: Source position or object <ARRAY|OBJECT>
// 1: Affiliation or raw CoT type <STRING> (default: "unknown")
// Supported affiliations: "friendly", "enemy", "hostile", "neutral", "unknown"
// 2: Marker kind <STRING> (default: "infantry")
// Supported kinds: "infantry", "tank", "car", "apc", "helicopter", "plane", "ship", "static"
// 3: Callsign <STRING> (default: "Report")
// 4: Remarks <STRING> (default: "")
// 5: Stale time in seconds <NUMBER> (default: 3600)
// 6: Optional registration scope <STRING> (default: "")
//
// Example:
// [cursorObject, "enemy", "tank", "Enemy Tank", "Reported enemy tank"] call armatak_fnc_report_marker;
// [screenToWorld [0.5, 0.5], "unknown", "infantry", "Unknown Contact", "Unknown contact reported", 7200] call armatak_fnc_report_marker;
// [cursorObject, "a-h-G-U-C-A-T", "Enemy Tank", "Reported enemy tank", 7200] call armatak_fnc_report_marker;
//
// Public: Yes
params [
["_source", objNull, [objNull, []]],
["_affiliationOrType", "unknown", [""]],
["_kindOrCallsign", "infantry", [""]],
["_callsignOrRemarks", "Report", [""]],
["_remarksOrStaleSeconds", "", ["", 0]],
["_staleSeconds", 3600, [0, ""]],
["_scope", "", [""]]
];
private _type = "";
private _callsign = _callsignOrRemarks;
private _remarks = _remarksOrStaleSeconds;
if (_staleSeconds isEqualType "") then {
_scope = _staleSeconds;
_staleSeconds = 3600;
};
if ((_affiliationOrType select [0, 2]) isEqualTo "a-") then {
_type = _affiliationOrType;
_callsign = _kindOrCallsign;
_remarks = _callsignOrRemarks;
if (_remarksOrStaleSeconds isEqualType 0) then {
_staleSeconds = _remarksOrStaleSeconds;
};
} else {
private _affiliation = switch (toLower _affiliationOrType) do {
case "friendly": {"f"};
case "enemy": {"h"};
case "hostile": {"h"};
case "neutral": {"n"};
default {"u"};
};
private _kind = switch (toLower _kindOrCallsign) do {
case "tank": {"G-U-C-A-T"};
case "car": {"G-U-C-I-M"};
case "apc": {"G-U-C-I-I"};
case "helicopter": {"A-M-H"};
case "plane": {"A-M-F"};
case "ship": {"S"};
case "static": {"G-U-C-F-M"};
default {"G-U-C-I"};
};
_type = "a-" + _affiliation + "-" + _kind;
if (_remarksOrStaleSeconds isEqualType 0) then {
_staleSeconds = _remarksOrStaleSeconds;
_remarks = "";
};
};
private _position = if (_source isEqualType objNull) then {
getPos _source
} else {
_source
};
if ((count _position) < 2) exitWith {
""
};
private _altitude = _position param [2, 0, [0]];
private _realLocation = [_position select 0, _position select 1, _altitude] call armatak_client_fnc_convertClientLocation;
private _uuid = "armatak" callExtension ["uuid", []] select 0;
private _payload = [
_uuid,
_type,
_realLocation select 0,
_realLocation select 1,
_realLocation select 2,
_callsign,
_staleSeconds max 1,
_remarks
];
"armatak" callExtension ["tcp_socket:cot:report_marker", [_payload]];
[_scope, _uuid, _type, _realLocation select 0, _realLocation select 1, _realLocation select 2] call armatak_fnc_register_cot;
_uuid

View File

@@ -6,7 +6,13 @@ if (!isNull _digitalPointer) then {
_digitalPointerPosition = _digitalPointer call armatak_client_fnc_extractClientPosition; _digitalPointerPosition = _digitalPointer call armatak_client_fnc_extractClientPosition;
_link_uid = [_unit] call armatak_fnc_extract_uuid; _link_uid = [_unit] call armatak_fnc_extract_uuid;
_contact_callsign = ([player] call armatak_fnc_extract_unit_callsign) + ".DP1"; _objectType = [_unit] call BIS_fnc_objectType;
_ownerCallsign = if ((_objectType select 0) == "Soldier") then {
[_unit] call armatak_fnc_extract_unit_callsign
} else {
[_unit] call armatak_fnc_extract_marker_callsign
};
_contact_callsign = _ownerCallsign + ".DP1";
_dpCot = [_link_uid, _contact_callsign, _digitalPointerPosition select 1, _digitalPointerPosition select 2, _digitalPointerPosition select 3]; _dpCot = [_link_uid, _contact_callsign, _digitalPointerPosition select 1, _digitalPointerPosition select 2, _digitalPointerPosition select 3];

View File

@@ -6,13 +6,20 @@ params["_unit"];
private _callsign = ""; private _callsign = "";
private _displayName = localize (getText (configOf _unit >> "displayName")); private _displayName = localize (getText (configOf _unit >> "displayName"));
private _markerCallsignOverride = _unit getVariable ["armatak_attribute_marker_callsign", ""];
if (_markerCallsignOverride isNotEqualTo "") exitWith {
_markerCallsignOverride
};
if (_displayName isEqualTo "") then { if (_displayName isEqualTo "") then {
_displayName = typeOf _unit; _displayName = typeOf _unit;
}; };
private _vehicleName = vehicleVarName _unit;
if ((([_unit] call BIS_fnc_objectType) select 0) == "Vehicle") then { if ((([_unit] call BIS_fnc_objectType) select 0) == "Vehicle") then {
_callsign = _displayName; _callsign = [_displayName, _vehicleName] select (_vehicleName isNotEqualTo "");
if (!isNull driver _unit) then { if (!isNull driver _unit) then {
_callsign = _displayName + " | " + ([name (driver _unit)] call armatak_fnc_shorten_name); _callsign = _displayName + " | " + ([name (driver _unit)] call armatak_fnc_shorten_name);
@@ -20,7 +27,13 @@ if ((([_unit] call BIS_fnc_objectType) select 0) == "Vehicle") then {
}; };
if (unitIsUAV _unit) then { if (unitIsUAV _unit) then {
_callsign = _displayName; _callsign = [_displayName, _vehicleName] select (_vehicleName isNotEqualTo "");
private _uavControl = UAVControl _unit;
private _controller = _uavControl param [0, objNull];
if (!isNull _controller) then {
_callsign = _callsign + " | " + ([name _controller] call armatak_fnc_shorten_name);
};
if (isUAVConnected _unit) then { if (isUAVConnected _unit) then {
_callsign = _callsign + " [ON]"; _callsign = _callsign + " [ON]";
@@ -29,10 +42,8 @@ if (unitIsUAV _unit) then {
} }
}; };
private _markerCallsignOverride = _unit getVariable ["armatak_attribute_marker_callsign", ""]; if (_callsign isEqualTo "") then {
_callsign = _displayName;
if (_markerCallsignOverride isNotEqualTo "") then {
_callsign = _markerCallsignOverride;
}; };
_callsign _callsign

View File

@@ -27,7 +27,7 @@ switch (str _side) do {
}; };
}; };
_unit_type = _unit call BIS_fnc_objectType; private _unit_type = _unit call BIS_fnc_objectType;
if ((_unit_type select 0) == "Soldier") then { if ((_unit_type select 0) == "Soldier") then {
switch (_unit_type select 1) do { switch (_unit_type select 1) do {
@@ -71,7 +71,7 @@ if ((_unit_type select 0) == "Soldier") then {
}; };
if ((typeOf (vehicle _unit) != typeOf _unit) or ((_unit_type select 0) == "Vehicle")) then { if ((typeOf (vehicle _unit) != typeOf _unit) or ((_unit_type select 0) == "Vehicle")) then {
_vehicle_type = (vehicle _unit) call BIS_fnc_objectType select 1; private _vehicle_type = (vehicle _unit) call BIS_fnc_objectType select 1;
switch (_vehicle_type) do { switch (_vehicle_type) do {
case "Car": { case "Car": {
_type = "G-U-C-I-M"; _type = "G-U-C-I-M";
@@ -111,10 +111,10 @@ if ((typeOf (vehicle _unit) != typeOf _unit) or ((_unit_type select 0) == "Vehic
_role = "a-" + _affiliation + "-" + _type; _role = "a-" + _affiliation + "-" + _type;
armatak_attribute_marker_type = _unit getVariable "armatak_attribute_marker_type"; private _markerTypeOverride = _unit getVariable ["armatak_attribute_marker_type", ""];
if (!isNil "armatak_attribute_marker_type" or armatak_attribute_marker_type != '') then { if (_markerTypeOverride isNotEqualTo "") then {
_role = armatak_attribute_marker_type; _role = _markerTypeOverride;
}; };
_role _role

View File

@@ -1,5 +1,7 @@
params["_unit"]; params["_unit"];
if (_unit getVariable ["armatak_disable_sensor_data", false]) exitWith {};
_target = getSensorTargets (_unit); _target = getSensorTargets (_unit);
{ {

View File

@@ -1,4 +1,4 @@
params ["_drone"]; params ["_drone", ["_cameraMode", "turret"]];
private _override = _drone getVariable ["armatak_uas_camera_data_override", []]; private _override = _drone getVariable ["armatak_uas_camera_data_override", []];
private _isLocalController = hasInterface && {!isNull player} && {(getConnectedUAV player) isEqualTo _drone}; private _isLocalController = hasInterface && {!isNull player} && {(getConnectedUAV player) isEqualTo _drone};
@@ -22,6 +22,7 @@ private _cameraDir = [];
private _spiASL = []; private _spiASL = [];
private _slantRange = 0; private _slantRange = 0;
if (_cameraMode isNotEqualTo "fpv") then {
private _laserTarget = laserTarget _drone; private _laserTarget = laserTarget _drone;
if (!isNull _laserTarget) then { if (!isNull _laserTarget) then {
private _laserTargetWorld = getPosWorld _laserTarget; private _laserTargetWorld = getPosWorld _laserTarget;
@@ -30,8 +31,9 @@ if (!isNull _laserTarget) then {
_cameraDir = _spiASL vectorDiff _originASL; _cameraDir = _spiASL vectorDiff _originASL;
_slantRange = _originASL vectorDistance _spiASL; _slantRange = _originASL vectorDistance _spiASL;
}; };
};
if (_cameraDir isEqualTo []) then { if (_cameraDir isEqualTo [] && {_cameraMode isNotEqualTo "fpv"}) then {
private _uavControl = UAVControl _drone; private _uavControl = UAVControl _drone;
private _controlledTurretPath = _uavControl param [1, []]; private _controlledTurretPath = _uavControl param [1, []];
private _candidateTurrets = []; private _candidateTurrets = [];

View File

@@ -9,15 +9,33 @@ if (isNil { missionNamespace getVariable "armatak_server_syncedUnits" }) then {
missionNamespace setVariable ["armatak_server_syncedUnits", []]; missionNamespace setVariable ["armatak_server_syncedUnits", []];
}; };
if (isNil { missionNamespace getVariable "armatak_server_clientClaimedEuds" }) then {
missionNamespace setVariable ["armatak_server_clientClaimedEuds", []];
};
GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits"; GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
[{ [{
GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits"; GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
private _clientClaimedEuds = missionNamespace getVariable ["armatak_server_clientClaimedEuds", []];
{ {
_objectType = _x call BIS_fnc_objectType; _objectType = _x call BIS_fnc_objectType;
switch (true) do { switch (true) do {
case ((_objectType select 0) == "Soldier"): { case ((_objectType select 0) == "Soldier"): {
private _uuid = _x call armatak_fnc_extract_uuid;
private _isClientEud = _x getVariable [QEGVAR(client,eudConnected), false];
if (_isClientEud) exitWith {
if !(_uuid in _clientClaimedEuds) then {
private _position = _x call armatak_client_fnc_extractClientPosition;
private _deleteCot = [_uuid, "a-f-G-U-C-I", _position select 1, _position select 2, _position select 3];
"armatak" callExtension ["tcp_socket:cot:delete", [_deleteCot]];
_clientClaimedEuds pushBack _uuid;
};
};
_clientClaimedEuds = _clientClaimedEuds - [_uuid];
_callsign = [_x] call armatak_fnc_extract_unit_callsign; _callsign = [_x] call armatak_fnc_extract_unit_callsign;
_group_name = [group _x] call armatak_fnc_extract_group_color; _group_name = [group _x] call armatak_fnc_extract_group_color;
_group_role = [_x] call armatak_fnc_extract_group_role; _group_role = [_x] call armatak_fnc_extract_group_role;
@@ -31,8 +49,8 @@ GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
_callsign = [_x] call armatak_fnc_extract_marker_callsign; _callsign = [_x] call armatak_fnc_extract_marker_callsign;
[_x, _atak_type, _callsign] call armatak_fnc_send_drone_cot; [_x, _atak_type, _callsign] call armatak_fnc_send_drone_cot;
[_x] call armatak_fnc_send_digital_pointer_cot;
_x call armatak_fnc_extract_sensor_data; _x call armatak_fnc_extract_sensor_data;
[_x] call armatak_fnc_send_digital_pointer_cot;
}; };
}; };
case ((_objectType select 0) == "Vehicle"): { case ((_objectType select 0) == "Vehicle"): {
@@ -41,6 +59,7 @@ GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
[_x, _atak_type, _callsign] call armatak_fnc_send_marker_cot; [_x, _atak_type, _callsign] call armatak_fnc_send_marker_cot;
_x call armatak_fnc_extract_sensor_data; _x call armatak_fnc_extract_sensor_data;
[_x] call armatak_fnc_send_digital_pointer_cot;
}; };
case ((_objectType select 0) == "VehicleAutonomous"): { case ((_objectType select 0) == "VehicleAutonomous"): {
if !(_x getVariable ["armatak_uav_mavlink_broadcasting", false]) then { if !(_x getVariable ["armatak_uav_mavlink_broadcasting", false]) then {
@@ -48,12 +67,14 @@ GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
_callsign = [_x] call armatak_fnc_extract_marker_callsign; _callsign = [_x] call armatak_fnc_extract_marker_callsign;
[_x, _atak_type, _callsign] call armatak_fnc_send_drone_cot; [_x, _atak_type, _callsign] call armatak_fnc_send_drone_cot;
[_x] call armatak_fnc_send_digital_pointer_cot;
_x call armatak_fnc_extract_sensor_data; _x call armatak_fnc_extract_sensor_data;
[_x] call armatak_fnc_send_digital_pointer_cot;
}; };
}; };
}; };
} forEach GVAR(syncedUnits); } forEach GVAR(syncedUnits);
missionNamespace setVariable ["armatak_server_clientClaimedEuds", _clientClaimedEuds];
}, 1, []] call CBA_fnc_addPerFrameHandler; }, 1, []] call CBA_fnc_addPerFrameHandler;
true true

View File

@@ -6,42 +6,281 @@ if (!hasInterface) exitWith {};
private _payload = [_data] call FUNC(parseMavlinkCallbackData); private _payload = [_data] call FUNC(parseMavlinkCallbackData);
private _uav = getConnectedUAV player; private _uav = getConnectedUAV player;
if (isNull _uav) then {
_uav = player getVariable [QGVAR(broadcastingUav), objNull];
};
if (isNull _uav) exitWith { if (isNull _uav) exitWith {
"armatak" callExtension ["log", [["warn", format ["Ignoring MAVLINK UDP callback %1 because no UAV is connected: %2", _function, _data]]]]; "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 _number = {
params ["_key", ["_default", 0]];
private _raw = _payload getOrDefault [_key, str _default];
private _value = parseNumber _raw;
if (!finite _value) exitWith {_default};
_value
};
private _uavGroup = {
params ["_vehicle"];
private _crew = crew _vehicle;
if (_crew isEqualTo []) exitWith {grpNull};
group (_crew select 0)
};
private _clearWaypoints = {
params ["_group"];
if (isNull _group) exitWith {};
for "_i" from ((count waypoints _group) - 1) to 0 step -1 do {
deleteWaypoint [_group, _i];
};
};
private _clearUavRoute = {
private _group = [_uav] call _uavGroup;
if (isNull _group) exitWith {false};
[_group] call _clearWaypoints;
_uav setVariable ["armatak_uas_mission_items", [], true];
true
};
private _geoToAtl = {
params ["_vehicle", "_lat", "_lon", ["_alt", -1]];
private _current = [_vehicle] call EFUNC(client,extractClientPosition);
private _currentLat = _current select 1;
private _currentLon = _current select 2;
private _currentAtl = getPosATL _vehicle;
private _northM = (_lat - _currentLat) * 111320;
private _eastM = (_lon - _currentLon) * (111320 * (cos _currentLat));
[
(_currentAtl select 0) + _eastM,
(_currentAtl select 1) + _northM,
if (_alt >= 0) then {_alt} else {_currentAtl select 2}
]
};
private _commandMove = {
params ["_vehicle", "_positionAtl", ["_type", "MOVE"], ["_radius", 80], ["_completion", 50]];
private _group = [_vehicle] call _uavGroup;
if (isNull _group) exitWith {false};
[_group] call _clearWaypoints;
_vehicle engineOn true;
_vehicle setVariable ["armatak_uas_armed", true, true];
_vehicle setFuel ((fuel _vehicle) max 0.1);
_vehicle flyInHeight ((_positionAtl select 2) max 10);
_vehicle doMove _positionAtl;
private _wp = _group addWaypoint [_positionAtl, 0];
_wp setWaypointType _type;
_wp setWaypointBehaviour "CARELESS";
_wp setWaypointCombatMode "BLUE";
_wp setWaypointSpeed "NORMAL";
_wp setWaypointCompletionRadius _completion;
if (_type == "LOITER") then {
_wp setWaypointLoiterRadius (_radius max 25);
_wp setWaypointLoiterType "CIRCLE_L";
};
true
};
private _appendMissionWaypoint = {
params ["_vehicle", "_positionAtl", "_command", "_seq", ["_radius", 80]];
private _group = [_vehicle] call _uavGroup;
if (isNull _group) exitWith {false};
private _type = switch (_command) do {
case 17;
case 18;
case 19;
case 31: {"LOITER"};
case 21: {"MOVE"};
default {"MOVE"};
};
private _wp = _group addWaypoint [_positionAtl, 0];
_wp setWaypointType _type;
_wp setWaypointBehaviour "CARELESS";
_wp setWaypointCombatMode "BLUE";
_wp setWaypointSpeed "NORMAL";
_wp setWaypointCompletionRadius 35;
if (_type == "LOITER") then {
_wp setWaypointLoiterRadius (_radius max 25);
_wp setWaypointLoiterType "CIRCLE_L";
};
if (_command == 21) then {
_wp setWaypointStatements ["true", "(vehicle this) land 'LAND'"];
};
private _items = _vehicle getVariable ["armatak_uas_mission_items", []];
_items pushBack [_seq, _command, _positionAtl];
_vehicle setVariable ["armatak_uas_mission_items", _items, true];
true
};
private _commandName = _payload getOrDefault ["command_name", "UNKNOWN"]; private _commandName = _payload getOrDefault ["command_name", "UNKNOWN"];
private _command = [_payload getOrDefault ["command", "-1"]] call BIS_fnc_parseNumber;
private _callsign = [_uav] call armatak_fnc_extract_marker_callsign;
private _applySpeed = {
params ["_speed"];
if (_speed <= 0) exitWith {false};
_uav limitSpeed _speed;
systemChat format ["ATAK SPEED %1m/s %2", round _speed, _callsign];
true
};
private _applyMode = {
params ["_mode"];
switch (_mode) do {
case 4: {
_uav engineOn true;
_uav setVariable ["armatak_uas_armed", true, true];
_uav setFuel ((fuel _uav) max 0.1);
systemChat format ["ATAK GUIDED %1", _callsign];
};
case 5: {
private _pos = getPosATL _uav;
[_uav, _pos, "LOITER", 80, 25] call _commandMove;
systemChat format ["ATAK LOITER %1", _callsign];
};
case 6;
case 21;
case 27: {
private _home = _uav getVariable ["armatak_uas_home_atl", getPosATL _uav];
_home set [2, ((_home select 2) max 60)];
[_uav, _home, "MOVE", 80, 60] call _commandMove;
systemChat format ["ATAK RTL %1", _callsign];
};
case 9: {
private _pos = getPosATL _uav;
_pos set [2, 0];
[_uav, _pos, "MOVE", 30, 20] call _commandMove;
_uav flyInHeight 0;
systemChat format ["ATAK LAND %1", _callsign];
};
default {
"armatak" callExtension ["log", [["info", format ["Unhandled MAVLINK mode %1 for UAV %2", _mode, _uav]]]];
};
};
};
private _setHomeFromGeo = {
params ["_lat", "_lon", "_alt"];
if (_lat == 0 && {_lon == 0}) exitWith {false};
private _homeAtl = [_uav, _lat, _lon, _alt] call _geoToAtl;
_uav setVariable ["armatak_uas_home_atl", _homeAtl, true];
_uav setVariable ["armatak_uas_home_geo", [_lat, _lon, _alt], true];
systemChat format ["ATAK HOME %1", _callsign];
true
};
switch (_function) do { switch (_function) do {
case "COMMAND_LONG": { case "COMMAND_LONG": {
switch (_command) do { switch (_command) do {
case 176: {
private _mode = ["param2", -1] call _number;
if (_mode < 0) then {
_mode = ["param1", -1] call _number;
};
[_mode] call _applyMode;
};
case 178: {
private _speed = ["param2", -1] call _number;
if (_speed <= 0) then {
_speed = ["param1", -1] call _number;
};
[_speed] call _applySpeed;
};
case 179: {
private _useCurrent = (["param1", 0] call _number) >= 1;
if (_useCurrent) then {
private _pos = [_uav] call EFUNC(client,extractClientPosition);
private _relAlt = ((getPosATL _uav) select 2) max 0;
private _homeAtl = getPosATL _uav;
_uav setVariable ["armatak_uas_home_atl", _homeAtl, true];
_uav setVariable ["armatak_uas_home_geo", [_pos select 1, _pos select 2, (_pos select 3) - _relAlt], true];
systemChat format ["ATAK HOME %1", _callsign];
} else {
[["param5", 0] call _number, ["param6", 0] call _number, ["param7", 0] call _number] call _setHomeFromGeo;
};
};
case 400: { case 400: {
private _armValue = parseNumber (_payload getOrDefault ["param1", "0"]); private _doArm = (["param1", 0] call _number) >= 1;
private _doArm = _armValue >= 1;
_uav engineOn _doArm; _uav engineOn _doArm;
_uav setVariable ["armatak_uas_armed", _doArm, true];
if (_doArm) then { if (_doArm) then {
_uav setFuel ((fuel _uav) max 0.1); _uav setFuel ((fuel _uav) max 0.1);
}; };
systemChat format ["ATAK %1 %2", ["DISARM", "ARM"] select _doArm, [_uav] call armatak_fnc_extract_marker_callsign]; systemChat format ["ATAK %1 %2", ["DISARM", "ARM"] select _doArm, _callsign];
"armatak" callExtension ["log", [["info", format ["Applied MAVLINK COMMAND_LONG %1 (%2) to UAV %3", _command, _commandName, _uav]]]]; "armatak" callExtension ["log", [["info", format ["Applied MAVLINK ARM=%1 to UAV %2", _doArm, _uav]]]];
}; };
case 22: { case 22: {
_uav engineOn true; private _alt = (["param7", 75] call _number) max 10;
_uav setFuel ((fuel _uav) max 0.1); private _pos = getPosATL _uav;
_uav flyInHeight 75; _pos set [2, _alt];
[_uav, _pos, "MOVE", 80, 25] call _commandMove;
if (_uav isKindOf "Helicopter" || {_uav isKindOf "VTOL_Base_F"} || {_uav isKindOf "Quadbike_01_F"}) then { _uav setVelocityModelSpace [0, 15, 8];
private _velocity = velocityModelSpace _uav; systemChat format ["ATAK TAKEOFF %1m %2", round _alt, _callsign];
_uav setVelocityModelSpace [ };
_velocity select 0, case 21: {
(_velocity select 1) max 15, [9] call _applyMode;
((_velocity select 2) max 0) + 8 };
]; case 20: {
[6] call _applyMode;
};
case 16: {
private _lat = ["param5", 0] call _number;
private _lon = ["param6", 0] call _number;
private _alt = ["param7", -1] call _number;
private _pos = [_uav, _lat, _lon, _alt] call _geoToAtl;
[_uav, _pos, "MOVE", 80, 50] call _commandMove;
systemChat format ["ATAK MOVE %1", _callsign];
};
case 17: {
private _lat = ["param5", 0] call _number;
private _lon = ["param6", 0] call _number;
private _alt = ["param7", -1] call _number;
private _radius = abs (["param3", 80] call _number);
private _pos = [_uav, _lat, _lon, _alt] call _geoToAtl;
[_uav, _pos, "LOITER", _radius, 30] call _commandMove;
systemChat format ["ATAK LOITER %1", _callsign];
};
case 43000: {
private _speed = ["param2", -1] call _number;
if (_speed <= 0) then {
_speed = ["param1", -1] call _number;
};
[_speed] call _applySpeed;
};
case 43001: {
private _alt = ["param1", -1] call _number;
if (_alt < 0) then {
_alt = ["param7", -1] call _number;
};
private _pos = getPosATL _uav;
_pos set [2, _alt max 10];
[_uav, _pos, "MOVE", 80, 25] call _commandMove;
systemChat format ["ATAK ALT %1m %2", round (_pos select 2), _callsign];
};
case 43002: {
private _heading = ["param1", -1] call _number;
if (_heading >= 0) then {
_uav setDir _heading;
systemChat format ["ATAK HDG %1 %2", round _heading, _callsign];
}; };
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 { default {
"armatak" callExtension ["log", [["info", format ["Unhandled MAVLINK COMMAND_LONG %1 (%2): %3", _command, _commandName, _data]]]]; "armatak" callExtension ["log", [["info", format ["Unhandled MAVLINK COMMAND_LONG %1 (%2): %3", _command, _commandName, _data]]]];
@@ -49,7 +288,87 @@ switch (_function) do {
}; };
}; };
case "COMMAND_INT": { case "COMMAND_INT": {
"armatak" callExtension ["log", [["info", format ["Received MAVLINK COMMAND_INT %1 (%2): %3", _command, _commandName, _data]]]]; private _lat = (["x", 0] call _number) / 1e7;
private _lon = (["y", 0] call _number) / 1e7;
private _alt = ["z", -1] call _number;
switch (_command) do {
case 16: {
private _pos = [_uav, _lat, _lon, _alt] call _geoToAtl;
[_uav, _pos, "MOVE", 80, 50] call _commandMove;
systemChat format ["ATAK MOVE %1", _callsign];
};
case 17;
case 192: {
private _radius = abs (["param3", 80] call _number);
private _direction = ["CIRCLE_L", "CIRCLE_R"] select ((["param4", 0] call _number) < 0);
private _pos = [_uav, _lat, _lon, _alt] call _geoToAtl;
private _type = ["MOVE", "LOITER"] select (_radius > 1);
[_uav, _pos, _type, _radius, 30] call _commandMove;
if (_type == "LOITER") then {
private _group = [_uav] call _uavGroup;
private _waypoints = waypoints _group;
if (_waypoints isNotEqualTo []) then {
(_waypoints select -1) setWaypointLoiterType _direction;
};
};
systemChat format ["ATAK %1 %2", _type, _callsign];
};
default {
"armatak" callExtension ["log", [["info", format ["Unhandled MAVLINK COMMAND_INT %1 (%2): %3", _command, _commandName, _data]]]];
};
};
};
case "MISSION_COUNT": {
private _count = ["count", 0] call _number;
[] call _clearUavRoute;
systemChat format ["ATAK ROUTE %1 pts %2", round _count, _callsign];
"armatak" callExtension ["log", [["info", format ["Receiving MAVLINK mission count=%1 for UAV %2", _count, _uav]]]];
};
case "MISSION_CLEAR_ALL": {
[] call _clearUavRoute;
systemChat format ["ATAK ROUTE CLEAR %1", _callsign];
};
case "MISSION_SET_CURRENT": {
private _seq = ["seq", 0] call _number;
"armatak" callExtension ["log", [["info", format ["MAVLINK mission set current seq=%1 for UAV %2", _seq, _uav]]]];
};
case "MISSION_ITEM";
case "MISSION_ITEM_INT": {
private _seq = ["seq", 0] call _number;
private _missionCommand = ["command", -1] call _number;
private _lat = ["lat", 0] call _number;
private _lon = ["lon", 0] call _number;
private _alt = ["alt", -1] call _number;
if (_lat == 0 && {_lon == 0}) exitWith {
"armatak" callExtension ["log", [["warn", format ["Ignoring MAVLINK mission item at zero coordinate: %1", _data]]]];
};
private _pos = [_uav, _lat, _lon, _alt] call _geoToAtl;
private _radius = abs (["param3", 80] call _number);
[_uav, _pos, _missionCommand, _seq, _radius] call _appendMissionWaypoint;
_uav engineOn true;
_uav setVariable ["armatak_uas_armed", true, true];
_uav setFuel ((fuel _uav) max 0.1);
_uav flyInHeight ((_pos select 2) max 10);
systemChat format ["ATAK ROUTE WP %1 %2", round _seq, _callsign];
"armatak" callExtension ["log", [["info", format ["Added MAVLINK mission item seq=%1 command=%2 posATL=%3 for UAV %4", _seq, _missionCommand, _pos, _uav]]]];
};
case "SET_HOME_POSITION": {
[["lat", 0] call _number, ["lon", 0] call _number, ["alt", 0] call _number] call _setHomeFromGeo;
};
case "SET_POSITION_TARGET_GLOBAL_INT": {
private _lat = ["lat", 0] call _number;
private _lon = ["lon", 0] call _number;
private _alt = ["alt", -1] call _number;
private _pos = [_uav, _lat, _lon, _alt] call _geoToAtl;
[_uav, _pos, "MOVE", 80, 40] call _commandMove;
systemChat format ["ATAK GUIDED MOVE %1", _callsign];
};
case "SET_MODE": {
private _mode = ["custom_mode", -1] call _number;
[_mode] call _applyMode;
}; };
case "COMMAND_ACK": { case "COMMAND_ACK": {
"armatak" callExtension ["log", [["info", format ["Received MAVLINK COMMAND_ACK %1 (%2): %3", _command, _commandName, _data]]]]; "armatak" callExtension ["log", [["info", format ["Received MAVLINK COMMAND_ACK %1 (%2): %3", _command, _commandName, _data]]]];

View File

@@ -2,7 +2,7 @@
params [["_uav", objNull, [objNull]]]; params [["_uav", objNull, [objNull]]];
private _defaultVideoUri = "rtsp://irontak.com:554/fpv"; private _defaultVideoUri = "rtsp://undefined:554/fpv";
private _activelyControlledUav = if (!isNull player) then {getConnectedUAV player} else {objNull}; private _activelyControlledUav = if (!isNull player) then {getConnectedUAV player} else {objNull};
private _normalize = { private _normalize = {
@@ -20,15 +20,19 @@ private _normalize = {
format ["rtp://%1", _url] format ["rtp://%1", _url]
}; };
if (!isNull _uav && {_activelyControlledUav isEqualTo _uav}) then { if (!isNull _uav) then {
private _objectVideoUrl = [_uav] call armatak_fnc_extract_marker_video_url; private _objectVideoUrl = [_uav] call armatak_fnc_extract_marker_video_url;
private _normalizedObjectVideoUrl = [_objectVideoUrl] call _normalize; private _normalizedObjectVideoUrl = [_objectVideoUrl] call _normalize;
if (_normalizedObjectVideoUrl isNotEqualTo "") exitWith { if (_normalizedObjectVideoUrl isNotEqualTo "") exitWith {
_normalizedObjectVideoUrl _normalizedObjectVideoUrl
}; };
private _activeSessionVideoUrl = player getVariable [QEGVAR(client,video_feed_url), ""];
private _normalizedActiveSessionVideoUrl = [_activeSessionVideoUrl] call _normalize;
if (_normalizedActiveSessionVideoUrl isNotEqualTo "") exitWith {
_normalizedActiveSessionVideoUrl
}; };
if (!isNull _uav) exitWith {
_defaultVideoUri _defaultVideoUri
}; };

View File

@@ -40,6 +40,11 @@ private _mavlinkAddress = player getVariable [QEGVAR(client,mavlink_address), ""
if (_mavlinkAddress isEqualTo "") exitWith {}; if (_mavlinkAddress isEqualTo "") exitWith {};
private _pos = [_uav] call EFUNC(client,extractClientPosition); private _pos = [_uav] call EFUNC(client,extractClientPosition);
private _relAlt = ((getPosATL _uav) select 2) max 0;
if (isNil {_uav getVariable "armatak_uas_home_atl"}) then {
_uav setVariable ["armatak_uas_home_atl", getPosATL _uav, true];
_uav setVariable ["armatak_uas_home_geo", [_pos select 1, _pos select 2, (_pos select 3) - _relAlt], true];
};
private _uuid = [_uav] call armatak_fnc_extract_uuid; private _uuid = [_uav] call armatak_fnc_extract_uuid;
private _callsign = [_uav] call armatak_fnc_extract_marker_callsign; private _callsign = [_uav] call armatak_fnc_extract_marker_callsign;
private _videoUri = [_uav] call FUNC(resolveVideoUri); private _videoUri = [_uav] call FUNC(resolveVideoUri);
@@ -48,8 +53,15 @@ private _up = vectorUp _uav;
private _yaw = getDir _uav; private _yaw = getDir _uav;
private _pitch = asin (((_dir select 2) max -1) min 1); private _pitch = asin (((_dir select 2) max -1) min 1);
private _roll = asin (((_up select 0) max -1) min 1); 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 _uavType = if (_uav isKindOf "Plane") then {1} else {[2, 3] select (_uav isKindOf "Helicopter")};
private _armed = _uav getVariable ["armatak_uas_armed", isEngineOn _uav];
if !(isEngineOn _uav) then {
_armed = false;
_uav setVariable ["armatak_uas_armed", false, true];
};
private _groundSpeed = abs (_pos select 6);
private _landed = (_relAlt <= 1.5) && {_groundSpeed <= 0.5};
private _batteryRemaining = round ((((fuel _uav) max 0) min 1) * 100);
private _gimbalRoll = 0; private _gimbalRoll = 0;
private _gimbalPitch = _pitch; private _gimbalPitch = _pitch;
@@ -59,34 +71,22 @@ private _vfov = _uav getVariable ["armatak_uas_vfov", (_hfov * 0.5625)];
private _imageLat = _pos select 1; private _imageLat = _pos select 1;
private _imageLon = _pos select 2; private _imageLon = _pos select 2;
private _imageAlt = _pos select 3; private _imageAlt = _pos select 3;
private _cameraData = [_uav, "turret"] call armatak_fnc_extract_uas_camera_data;
private _uavControl = UAVControl _uav;
private _controlledTurretPath = _uavControl param [1, []];
private _hasTurretCamera = ((_controlledTurretPath isEqualType []) && {_controlledTurretPath isNotEqualTo []}) || {(allTurrets _uav) isNotEqualTo []};
private _laser = laserTarget _uav; if (_cameraData isEqualType [] && {(count _cameraData) >= 6}) then {
if (!isNull _laser) then { _gimbalYaw = _cameraData param [0, _yaw];
private _originASL = getPosASL _uav; _gimbalPitch = _cameraData param [1, _pitch];
private _targetWorld = getPosWorld _laser; _hfov = _cameraData param [2, _hfov];
private _targetAslZ = (getPosASL _laser) select 2; _vfov = _uav getVariable ["armatak_uas_vfov", (_hfov * 0.5625)];
private _spiASL = [_targetWorld select 0, _targetWorld select 1, _targetAslZ];
private _los = _spiASL vectorDiff _originASL; private _spiGeo = _cameraData param [5, []];
private _losMag = vectorMagnitude _los; if (_spiGeo isEqualType [] && {(count _spiGeo) >= 3}) then {
_imageLat = _spiGeo select 0;
if (_losMag > 0) then { _imageLon = _spiGeo select 1;
_los = _los vectorMultiply (1 / _losMag); _imageAlt = _spiGeo select 2;
private _dirX = _los select 0;
private _dirY = _los select 1;
private _dirZ = _los select 2;
private _horizontalMag = sqrt ((_dirX * _dirX) + (_dirY * _dirY));
private _camYaw = ((_dirX atan2 _dirY) + 360) mod 360;
private _camPitch = _dirZ atan2 (_horizontalMag max 0.001);
_gimbalPitch = _camPitch;
_gimbalYaw = _camYaw;
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;
}; };
}; };
@@ -104,7 +104,8 @@ private _systemPayload = [
_roll, _roll,
_pitch, _pitch,
_yaw, _yaw,
parseNumber isEngineOn _uav, parseNumber _armed,
parseNumber _landed,
_gimbalRoll, _gimbalRoll,
_gimbalPitch, _gimbalPitch,
_gimbalYaw, _gimbalYaw,
@@ -113,7 +114,9 @@ private _systemPayload = [
_vfov, _vfov,
_imageLat, _imageLat,
_imageLon, _imageLon,
_imageAlt _imageAlt,
parseNumber _hasTurretCamera,
_batteryRemaining
]; ];
"armatak" callExtension ["uas:send_uas_system", [_systemPayload]]; "armatak" callExtension ["uas:send_uas_system", [_systemPayload]];

View File

@@ -18,6 +18,7 @@ pub struct CursorOverTime {
pub link_uid: Option<String>, pub link_uid: Option<String>,
pub remarker: Option<String>, pub remarker: Option<String>,
pub video_url: Option<String>, pub video_url: Option<String>,
pub stale_seconds: Option<i64>,
} }
impl CursorOverTime { impl CursorOverTime {
@@ -34,8 +35,9 @@ impl CursorOverTime {
let created_time = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true); let created_time = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
let stale_time = let stale_seconds = self.stale_seconds.unwrap_or(360).max(1);
(Utc::now() + Duration::seconds(360)).to_rfc3339_opts(SecondsFormat::Millis, true); let stale_time = (Utc::now() + Duration::seconds(stale_seconds))
.to_rfc3339_opts(SecondsFormat::Millis, true);
let mut xml = String::new(); let mut xml = String::new();

45
src/cot/delete.rs Normal file
View File

@@ -0,0 +1,45 @@
use arma_rs::{FromArma, FromArmaError};
use chrono::{Duration, SecondsFormat, Utc};
pub struct DeleteCoTPayload {
pub target_uid: String,
pub target_type: String,
pub point_lat: f64,
pub point_lon: f64,
pub point_hae: f32,
}
impl FromArma for DeleteCoTPayload {
fn from_arma(data: String) -> Result<DeleteCoTPayload, FromArmaError> {
let (target_uid, target_type, point_lat, point_lon, point_hae) =
<(String, String, f64, f64, f32)>::from_arma(data)?;
Ok(Self {
target_uid,
target_type,
point_lat,
point_lon,
point_hae,
})
}
}
impl DeleteCoTPayload {
pub fn to_xml(&self) -> String {
let created_time = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
let stale_time =
(Utc::now() + Duration::seconds(60)).to_rfc3339_opts(SecondsFormat::Millis, true);
format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\" ?><event type=\"t-x-d-d\" version=\"2.0\" how=\"m-g\" uid=\"{}.delete\" time=\"{}\" start=\"{}\" stale=\"{}\"><point ce=\"9999999\" le=\"9999999\" hae=\"{}\" lat=\"{}\" lon=\"{}\" /><detail><link uid=\"{}\" type=\"{}\" relation=\"none\" /><__forcedelete /></detail></event>",
self.target_uid,
created_time,
created_time,
stale_time,
self.point_hae,
self.point_lat,
self.point_lon,
self.target_uid,
self.target_type
)
}
}

View File

@@ -41,7 +41,7 @@ impl DigitalPointerPayload {
link_uid: Some(self.link_uid.clone()), link_uid: Some(self.link_uid.clone()),
remarker: None, remarker: None,
video_url: None, video_url: None,
stale_seconds: Some(7),
} }
} }
} }

View File

@@ -1 +1,3 @@
pub mod circle; pub mod circle;
pub mod route;
pub mod shape;

149
src/cot/draws/route.rs Normal file
View File

@@ -0,0 +1,149 @@
use arma_rs::{FromArma, FromArmaError};
use uuid::Uuid;
#[derive(Clone)]
struct RoutePoint {
lat: f64,
lon: f64,
hae: f32,
}
pub struct RoutePayload {
pub uuid: String,
pub points: String,
pub callsign: String,
pub stale_seconds: i64,
pub color: i32,
pub stroke_weight: f64,
pub method: String,
pub route_type: String,
pub direction: String,
pub checkpoint_interval: usize,
}
impl FromArma for RoutePayload {
fn from_arma(data: String) -> Result<Self, FromArmaError> {
let (
uuid,
points,
callsign,
stale_seconds,
color,
stroke_weight,
method,
route_type,
direction,
checkpoint_interval,
) = <(
String,
String,
String,
i64,
i32,
f64,
String,
String,
String,
i32,
)>::from_arma(data)?;
Ok(Self {
uuid,
points,
callsign,
stale_seconds,
color,
stroke_weight,
method,
route_type,
direction,
checkpoint_interval: checkpoint_interval.max(1) as usize,
})
}
}
impl RoutePayload {
pub fn to_xml(&self, now: &str, stale: &str) -> String {
let points = parse_points(&self.points);
let links = route_links(&self.callsign, &points, self.checkpoint_interval);
format!(
r#"<?xml version="1.0" encoding="UTF-8" ?><event version="2.0" uid="{uid}" type="b-m-r" time="{now}" start="{now}" stale="{stale}" how="h-e" access="Undefined"><point lat="0.0" lon="0.0" hae="9999999.0" ce="9999999.0" le="9999999.0"/><detail>{links}<link_attr planningmethod="{direction}" color="{color}" method="{method}" prefix="CP" style="0" type="Vehicle" stroke="{stroke_weight}" direction="{direction}" routetype="{route_type}" order="Ascending Check Points"/><creator uid="ARMATAK" callsign="ArmaTAK" time="{now}" type="a-f-G-U-C"/><strokeColor value="{color}"/><strokeWeight value="{stroke_weight}"/><strokeStyle value="solid"/><labels_on value="false"/><__routeinfo><__navcues/></__routeinfo><color value="{color}"/><remarks/><contact callsign="{callsign}"/><archive/><height_unit>1</height_unit></detail></event>"#,
uid = escape_attr(&self.uuid),
now = now,
stale = stale,
links = links,
direction = escape_attr(&self.direction),
color = self.color,
method = escape_attr(&self.method),
stroke_weight = self.stroke_weight.max(1.0),
route_type = escape_attr(&self.route_type),
callsign = escape_attr(&self.callsign)
)
}
}
fn route_links(callsign: &str, points: &[RoutePoint], checkpoint_interval: usize) -> String {
let mut xml = String::new();
let last_index = points.len().saturating_sub(1);
let mut checkpoint_number = 1;
for (index, point) in points.iter().enumerate() {
let is_start = index == 0;
let is_end = index == last_index;
let is_checkpoint = !is_start && !is_end && index % checkpoint_interval == 0;
let point_callsign = if is_start {
format!("{} SP", callsign)
} else if is_end {
"VDO".to_string()
} else if is_checkpoint {
let name = format!("CP{}", checkpoint_number);
checkpoint_number += 1;
name
} else {
String::new()
};
let link_type = if point_callsign.is_empty() {
"b-m-p-c"
} else {
"b-m-p-w"
};
xml.push_str(&format!(
r#"<link uid="{link_uid}" callsign="{callsign}" type="{link_type}" point="{lat},{lon},{hae}" remarks="" relation="c"/>"#,
link_uid = escape_attr(&Uuid::new_v4().to_string()),
callsign = escape_attr(&point_callsign),
link_type = link_type,
lat = point.lat,
lon = point.lon,
hae = point.hae
));
}
xml
}
fn parse_points(raw: &str) -> Vec<RoutePoint> {
raw.split(';')
.filter_map(|entry| {
let parts: Vec<_> = entry.split(',').collect();
if parts.len() != 3 {
return None;
}
Some(RoutePoint {
lat: parts[0].parse().ok()?,
lon: parts[1].parse().ok()?,
hae: parts[2].parse().ok()?,
})
})
.collect()
}
fn escape_attr(value: &str) -> String {
value
.replace('&', "&amp;")
.replace('"', "&quot;")
.replace('<', "&lt;")
.replace('>', "&gt;")
}

317
src/cot/draws/shape.rs Normal file
View File

@@ -0,0 +1,317 @@
use arma_rs::{FromArma, FromArmaError};
#[derive(Clone)]
struct DrawPoint {
lat: f64,
lon: f64,
hae: f32,
}
pub struct DrawEllipsePayload {
pub uuid: String,
pub cot_type: String,
pub center_lat: f64,
pub center_lon: f64,
pub center_hae: f32,
pub major: f64,
pub minor: f64,
pub angle: f32,
pub callsign: String,
pub stale_seconds: i64,
pub stroke_color: i32,
pub fill_color: i32,
pub stroke_weight: f64,
pub milsym: String,
}
pub struct DrawLinksPayload {
pub uuid: String,
pub cot_type: String,
pub center_lat: f64,
pub center_lon: f64,
pub center_hae: f32,
pub points: String,
pub callsign: String,
pub stale_seconds: i64,
pub stroke_color: i32,
pub fill_color: i32,
pub stroke_weight: f64,
pub stroke_style: String,
pub closed: bool,
pub milsym: String,
}
impl FromArma for DrawEllipsePayload {
fn from_arma(data: String) -> Result<Self, FromArmaError> {
let (
uuid,
cot_type,
center_lat,
center_lon,
center_hae,
major,
minor,
angle,
callsign,
stale_seconds,
stroke_color,
fill_color,
stroke_weight,
milsym,
) = <(
String,
String,
f64,
f64,
f32,
f64,
f64,
f32,
String,
i64,
i32,
i32,
f64,
String,
)>::from_arma(data)?;
Ok(Self {
uuid,
cot_type,
center_lat,
center_lon,
center_hae,
major,
minor,
angle,
callsign,
stale_seconds,
stroke_color,
fill_color,
stroke_weight,
milsym,
})
}
}
impl FromArma for DrawLinksPayload {
fn from_arma(data: String) -> Result<Self, FromArmaError> {
let (
uuid,
cot_type,
center_lat,
center_lon,
center_hae,
points,
callsign,
stale_seconds,
stroke_color,
fill_color,
stroke_weight,
stroke_style,
closed,
milsym,
) = <(
String,
String,
f64,
f64,
f32,
String,
String,
i64,
i32,
i32,
f64,
String,
bool,
String,
)>::from_arma(data)?;
Ok(Self {
uuid,
cot_type,
center_lat,
center_lon,
center_hae,
points,
callsign,
stale_seconds,
stroke_color,
fill_color,
stroke_weight,
stroke_style,
closed,
milsym,
})
}
}
impl DrawEllipsePayload {
pub fn to_xml(&self, now: &str, stale: &str) -> String {
shape_event(
&self.uuid,
&self.cot_type,
self.center_lat,
self.center_lon,
self.center_hae,
now,
stale,
&self.callsign,
&ellipse_shape_detail(
&self.uuid,
self.major,
self.minor,
self.angle,
self.stroke_color,
self.fill_color,
self.stroke_weight,
),
self.stroke_color,
self.fill_color,
self.stroke_weight,
"solid",
&self.milsym,
)
}
}
impl DrawLinksPayload {
pub fn to_xml(&self, now: &str, stale: &str) -> String {
let points = parse_points(&self.points);
let shape_detail = links_detail(&points, self.closed);
shape_event(
&self.uuid,
&self.cot_type,
self.center_lat,
self.center_lon,
self.center_hae,
now,
stale,
&self.callsign,
&shape_detail,
self.stroke_color,
self.fill_color,
self.stroke_weight,
&self.stroke_style,
&self.milsym,
)
}
}
fn shape_event(
uid: &str,
cot_type: &str,
lat: f64,
lon: f64,
hae: f32,
now: &str,
stale: &str,
callsign: &str,
shape_detail: &str,
stroke_color: i32,
fill_color: i32,
stroke_weight: f64,
stroke_style: &str,
milsym: &str,
) -> String {
let milsym_detail = if milsym.trim().is_empty() {
String::new()
} else {
format!(r#"<__milsym id="{}" />"#, escape_attr(milsym))
};
format!(
r#"<?xml version="1.0" encoding="UTF-8" ?><event version="2.0" uid="{uid}" type="{cot_type}" time="{now}" start="{now}" stale="{stale}" how="h-e" access="Undefined"><point lat="{lat}" lon="{lon}" hae="{hae}" ce="10.9" le="9999999.0" /><detail>{shape_detail}<__shapeExtras cpvis="true" editable="true" /><contact callsign="{callsign}" /><remarks /><archive /><labels_on value="true" /><strokeColor value="{stroke_color}" /><strokeWeight value="{stroke_weight}" /><strokeStyle value="{stroke_style}" /><fillColor value="{fill_color}" /><precisionlocation altsrc="GPS" geopointsrc="GPS" />{milsym_detail}</detail></event>"#,
uid = escape_attr(uid),
cot_type = escape_attr(cot_type),
now = now,
stale = stale,
lat = lat,
lon = lon,
hae = hae,
shape_detail = shape_detail,
callsign = escape_attr(callsign),
stroke_color = stroke_color,
stroke_weight = stroke_weight,
stroke_style = escape_attr(stroke_style),
fill_color = fill_color,
milsym_detail = milsym_detail
)
}
fn ellipse_shape_detail(
uid: &str,
major: f64,
minor: f64,
angle: f32,
stroke_color: i32,
fill_color: i32,
stroke_weight: f64,
) -> String {
format!(
r#"<shape><ellipse major="{major}" minor="{minor}" angle="{angle}" /><link uid="{style_uid}" type="b-x-KmlStyle" relation="p-c"><Style><LineStyle><color>{stroke_hex}</color><width>{stroke_weight}</width></LineStyle><PolyStyle><color>{fill_hex}</color></PolyStyle></Style></link></shape>"#,
major = major,
minor = minor,
angle = angle,
style_uid = escape_attr(&format!("{}.Style", uid)),
stroke_hex = argb_hex(stroke_color),
stroke_weight = stroke_weight,
fill_hex = argb_hex(fill_color)
)
}
fn links_detail(points: &[DrawPoint], closed: bool) -> String {
let mut detail = String::new();
for point in points {
detail.push_str(&link_detail(point));
}
if closed {
if let Some(first) = points.first() {
detail.push_str(&link_detail(first));
}
}
detail
}
fn link_detail(point: &DrawPoint) -> String {
format!(
r#"<link point="{lat},{lon},{hae}" />"#,
lat = point.lat,
lon = point.lon,
hae = point.hae
)
}
fn parse_points(raw: &str) -> Vec<DrawPoint> {
raw.split(';')
.filter_map(|entry| {
let parts: Vec<_> = entry.split(',').collect();
if parts.len() != 3 {
return None;
}
Some(DrawPoint {
lat: parts[0].parse().ok()?,
lon: parts[1].parse().ok()?,
hae: parts[2].parse().ok()?,
})
})
.collect()
}
fn argb_hex(color: i32) -> String {
format!("{:08x}", color as u32)
}
fn escape_attr(value: &str) -> String {
value
.replace('&', "&amp;")
.replace('"', "&quot;")
.replace('<', "&lt;")
.replace('>', "&gt;")
}

View File

@@ -58,7 +58,7 @@ impl EudCoTPayload {
link_uid: None, link_uid: None,
remarker: None, remarker: None,
video_url: None, video_url: None,
stale_seconds: None,
} }
} }
} }

View File

@@ -55,7 +55,7 @@ impl ExternalPositionPayload {
link_uid: None, link_uid: None,
remarker: Some(self.remarker.clone()), remarker: Some(self.remarker.clone()),
video_url: None, video_url: None,
stale_seconds: None,
} }
} }
} }

59
src/cot/lrf.rs Normal file
View File

@@ -0,0 +1,59 @@
use arma_rs::{FromArma, FromArmaError};
use chrono::{SecondsFormat, Utc};
pub struct LaserRangeFinderPayload {
pub uid: String,
pub distance_meters: f64,
pub azimuth_degrees: f64,
pub elevation_degrees: f64,
}
impl FromArma for LaserRangeFinderPayload {
fn from_arma(data: String) -> Result<LaserRangeFinderPayload, FromArmaError> {
let (uid, distance_meters, azimuth_degrees, elevation_degrees) =
<(String, f64, f64, f64)>::from_arma(data)?;
Ok(Self {
uid,
distance_meters,
azimuth_degrees,
elevation_degrees,
})
}
}
impl LaserRangeFinderPayload {
pub fn to_lrf_message(&self) -> String {
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
format!(
"1,{},{},{:.2},{:.2},{:.2}",
self.uid,
timestamp,
self.distance_meters.max(0.0),
normalize_degrees(self.azimuth_degrees),
self.elevation_degrees
)
}
}
pub struct LaserRangeFinderClearPayload {
pub uid: String,
}
impl FromArma for LaserRangeFinderClearPayload {
fn from_arma(data: String) -> Result<LaserRangeFinderClearPayload, FromArmaError> {
let uid = String::from_arma(data)?;
Ok(Self { uid })
}
}
impl LaserRangeFinderClearPayload {
pub fn to_lrf_message(&self) -> String {
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
format!("1,{},{},RANGE_ERROR", self.uid, timestamp)
}
}
fn normalize_degrees(degrees: f64) -> f64 {
degrees.rem_euclid(360.0)
}

View File

@@ -1,9 +1,12 @@
pub mod cot; pub mod cot;
pub mod delete;
pub mod digital_pointer; pub mod digital_pointer;
pub mod draws; pub mod draws;
pub mod eud; pub mod eud;
pub mod gps; pub mod gps;
pub mod lrf;
pub mod message; pub mod message;
pub mod nato; pub mod nato;
pub mod report_marker;
pub mod uas; pub mod uas;
pub mod video; pub mod video;

View File

@@ -87,6 +87,7 @@ impl MarkerCoTPayload {
link_uid: None, link_uid: None,
remarker: None, remarker: None,
video_url: self.video_url.clone(), video_url: self.video_url.clone(),
stale_seconds: None,
} }
} }
} }

63
src/cot/report_marker.rs Normal file
View File

@@ -0,0 +1,63 @@
use arma_rs::{FromArma, FromArmaError};
use super::cot::CursorOverTime;
pub struct ReportMarkerCoTPayload {
pub uuid: String,
pub r#type: String,
pub point_lat: f64,
pub point_lon: f64,
pub point_hae: f32,
pub contact_callsign: String,
pub stale_seconds: i64,
pub remarks: String,
}
impl FromArma for ReportMarkerCoTPayload {
fn from_arma(data: String) -> Result<ReportMarkerCoTPayload, FromArmaError> {
let (
uuid,
r#type,
point_lat,
point_lon,
point_hae,
contact_callsign,
stale_seconds,
remarks,
) = <(String, String, f64, f64, f32, String, i64, String)>::from_arma(data)?;
Ok(Self {
uuid,
r#type,
point_lat,
point_lon,
point_hae,
contact_callsign,
stale_seconds,
remarks,
})
}
}
impl ReportMarkerCoTPayload {
pub fn to_cot(&self) -> CursorOverTime {
CursorOverTime {
uuid: Some(self.uuid.clone()),
r#type: Some(self.r#type.clone()),
point_lat: self.point_lat,
point_lon: self.point_lon,
point_hae: self.point_hae,
point_ce: None,
point_le: None,
contact_callsign: self.contact_callsign.clone(),
group_name: None,
group_role: None,
track_course: None,
track_speed: None,
link_uid: None,
remarker: Some(self.remarks.clone()),
video_url: None,
stale_seconds: Some(self.stale_seconds),
}
}
}

View File

@@ -131,8 +131,8 @@ impl UasPlatformCoTPayload {
None => (escape_xml(&self.vehicle_type_tag), None), None => (escape_xml(&self.vehicle_type_tag), None),
}; };
let now = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true); let now = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
let stale = let stale = (Utc::now() + Duration::milliseconds(3500))
(Utc::now() + Duration::milliseconds(3500)).to_rfc3339_opts(SecondsFormat::Millis, true); .to_rfc3339_opts(SecondsFormat::Millis, true);
let mut xml = String::new(); let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"); xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
@@ -153,8 +153,7 @@ impl UasPlatformCoTPayload {
xml.push_str("<_uastool extendedCot=\"true\" activeRoute=\"false\"/>"); xml.push_str("<_uastool extendedCot=\"true\" activeRoute=\"false\"/>");
xml.push_str(&format!( xml.push_str(&format!(
"<track course=\"{}\" slope=\"0.0\" speed=\"{}\"/>", "<track course=\"{}\" slope=\"0.0\" speed=\"{}\"/>",
self.track_course, self.track_course, self.track_speed,
self.track_speed,
)); ));
xml.push_str(&format!( xml.push_str(&format!(
"<sensor elevation=\"{}\" vfov=\"{}\" north=\"0.0\" roll=\"0.0\" range=\"{}\" azimuth=\"{}\" fov=\"{}\" type=\"r-e\" version=\"0.6\"/>", "<sensor elevation=\"{}\" vfov=\"{}\" north=\"0.0\" roll=\"0.0\" range=\"{}\" azimuth=\"{}\" fov=\"{}\" type=\"r-e\" version=\"0.6\"/>",
@@ -186,7 +185,10 @@ impl UasPlatformCoTPayload {
} else { } else {
xml.push_str("<__video></__video>"); xml.push_str("<__video></__video>");
} }
xml.push_str(&format!("<link uid=\"{}\" type=\"a-f-G-U-C\" relation=\"p-p\" />", link_uid)); xml.push_str(&format!(
"<link uid=\"{}\" type=\"a-f-G-U-C\" relation=\"p-p\" />",
link_uid
));
xml.push_str("</detail></event>"); xml.push_str("</detail></event>");
xml xml
} }
@@ -326,5 +328,3 @@ impl UasSensorCoTPayload {
xml xml
} }
} }

View File

@@ -13,7 +13,9 @@ fn parse_video_url(url: &str) -> Option<(String, String, String, String)> {
Some((authority, path)) => (authority, format!("/{}", path)), Some((authority, path)) => (authority, format!("/{}", path)),
None => (rest, String::new()), None => (rest, String::new()),
}; };
let host_port = authority.rsplit_once('@').map_or(authority, |(_, host_port)| host_port); let host_port = authority
.rsplit_once('@')
.map_or(authority, |(_, host_port)| host_port);
let (address, port) = host_port.rsplit_once(':')?; let (address, port) = host_port.rsplit_once(':')?;
if protocol.is_empty() || address.is_empty() || port.is_empty() { if protocol.is_empty() || address.is_empty() || port.is_empty() {
@@ -35,10 +37,7 @@ pub fn video_detail_xml(video_url: &str, uid: &str, callsign: &str) -> String {
} }
let Some((protocol, address, port, path)) = parse_video_url(trimmed_url) else { let Some((protocol, address, port, path)) = parse_video_url(trimmed_url) else {
return format!( return format!("<__video url=\"{}\"/>", escape_xml_attribute(trimmed_url));
"<__video url=\"{}\"/>",
escape_xml_attribute(trimmed_url)
);
}; };
format!( format!(

View File

@@ -1,10 +1,10 @@
use arma_rs::{arma, Extension, Group}; use arma_rs::{arma, Extension, Group};
use rustls::crypto::aws_lc_rs; use rustls::crypto::aws_lc_rs;
mod uas;
mod mdns; mod mdns;
mod structs; mod structs;
mod tcp; mod tcp;
mod tests; mod tests;
mod uas;
mod udp_socket; mod udp_socket;
mod video_stream; mod video_stream;
@@ -59,8 +59,17 @@ pub fn init() -> Extension {
"udp_socket", "udp_socket",
Group::new() Group::new()
.command("start", udp_socket::start) .command("start", udp_socket::start)
.command("start_lrf", udp_socket::start_lrf)
.command("start_cot", udp_socket::start_cot)
.command("send_payload", udp_socket::send_payload) .command("send_payload", udp_socket::send_payload)
.command("send_gps_cot", udp_socket::send_gps_cot) .command("send_gps_cot", udp_socket::send_gps_cot)
.command("send_eud_cot", udp_socket::send_eud_cot)
.command("send_lrf", udp_socket::send_lrf)
.command("clear_lrf", udp_socket::clear_lrf)
.command(
"send_digital_pointer_cot",
udp_socket::send_digital_pointer_cot,
)
.command("stop", udp_socket::stop), .command("stop", udp_socket::stop),
) )
.group( .group(
@@ -76,7 +85,9 @@ pub fn init() -> Extension {
Group::new() Group::new()
.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("report_marker", tcp::cot::send_report_marker_cot)
.command("digital_pointer", tcp::cot::send_digital_pointer_cot) .command("digital_pointer", tcp::cot::send_digital_pointer_cot)
.command("delete", tcp::cot::send_delete_cot)
.command("chat", tcp::cot::send_message_cot) .command("chat", tcp::cot::send_message_cot)
.command("uas_platform", tcp::cot::send_uas_platform_cot) .command("uas_platform", tcp::cot::send_uas_platform_cot)
.command("uas_video", tcp::cot::send_uas_video_cot) .command("uas_video", tcp::cot::send_uas_video_cot)
@@ -89,7 +100,8 @@ pub fn init() -> Extension {
.command("ellipse", tcp::draw::send_ellipse_cot) .command("ellipse", tcp::draw::send_ellipse_cot)
.command("rectangle", tcp::draw::send_rectangle_cot) .command("rectangle", tcp::draw::send_rectangle_cot)
.command("free", tcp::draw::send_freedraw_cot) .command("free", tcp::draw::send_freedraw_cot)
.command("vector", tcp::draw::send_vectordraw_cot), .command("vector", tcp::draw::send_vectordraw_cot)
.command("route", tcp::draw::send_route_cot),
), ),
) )
.group( .group(

View File

@@ -23,7 +23,13 @@ fn detect_local_ipv4() -> Result<Ipv4Addr, String> {
fn sanitize_label(value: &str, fallback: &str) -> String { fn sanitize_label(value: &str, fallback: &str) -> String {
let mut sanitized = value let mut sanitized = value
.chars() .chars()
.map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '-' }) .map(|c| {
if c.is_ascii_alphanumeric() || c == '-' {
c
} else {
'-'
}
})
.collect::<String>() .collect::<String>()
.trim_matches('-') .trim_matches('-')
.to_string(); .to_string();
@@ -67,7 +73,13 @@ fn push_record(buf: &mut Vec<u8>, name: &str, rr_type: u16, rr_class: u16, ttl:
buf.extend_from_slice(rdata); buf.extend_from_slice(rdata);
} }
fn build_mdns_packet(instance_name: &str, host_name: &str, ip: Ipv4Addr, port: u16, video_uri: &str) -> Vec<u8> { fn build_mdns_packet(
instance_name: &str,
host_name: &str,
ip: Ipv4Addr,
port: u16,
video_uri: &str,
) -> Vec<u8> {
let service_type = "_mavlink._udp.local"; let service_type = "_mavlink._udp.local";
let instance_fqdn = format!("{}.{}", instance_name, service_type); let instance_fqdn = format!("{}.{}", instance_name, service_type);
let host_fqdn = format!("{}.local", host_name); let host_fqdn = format!("{}.local", host_name);
@@ -122,7 +134,11 @@ pub fn start_uas_advertisement(
let local_ip = match detect_local_ipv4() { let local_ip = match detect_local_ipv4() {
Ok(ip) => ip, Ok(ip) => ip,
Err(error) => { Err(error) => {
let _ = ctx.callback_data("MDNS ERROR", "Failed to determine local IPv4", error.clone()); let _ = ctx.callback_data(
"MDNS ERROR",
"Failed to determine local IPv4",
error.clone(),
);
return "mdns local IPv4 error"; return "mdns local IPv4 error";
} }
}; };
@@ -161,7 +177,10 @@ pub fn start_uas_advertisement(
loop { loop {
match socket.send_to(&packet, multicast_addr) { match socket.send_to(&packet, multicast_addr) {
Ok(size) => info!("Sent mDNS UAS advertisement ({} bytes) to {}", size, multicast_addr), Ok(size) => info!(
"Sent mDNS UAS advertisement ({} bytes) to {}",
size, multicast_addr
),
Err(error) => info!("Failed sending mDNS UAS advertisement: {}", error), Err(error) => info!("Failed sending mDNS UAS advertisement: {}", error),
} }
@@ -172,7 +191,10 @@ pub fn start_uas_advertisement(
} }
} }
info!("Stopped mDNS UAS advertisement for instance={}", safe_instance); info!(
"Stopped mDNS UAS advertisement for instance={}",
safe_instance
);
}); });
let _ = ctx.callback_data( let _ = ctx.callback_data(

View File

@@ -54,9 +54,7 @@ fn send_over_stream(
) -> Result<(), String> { ) -> Result<(), String> {
let message_len = message.len(); let message_len = message.len();
info!("Sending TCP payload ({} bytes)", message_len); info!("Sending TCP payload ({} bytes)", message_len);
stream stream.write_message(message.as_bytes()).map_err(|e| {
.write_message(message.as_bytes())
.map_err(|e| {
let message = e.to_string(); let message = e.to_string();
let _ = context.callback_data( let _ = context.callback_data(
"TCP SOCKET ERROR", "TCP SOCKET ERROR",
@@ -145,10 +143,14 @@ impl TcpClient {
let mut pending_messages: VecDeque<(String, Context)> = VecDeque::new(); let mut pending_messages: VecDeque<(String, Context)> = VecDeque::new();
let (connect_tx, connect_rx) = mpsc::channel(); let (connect_tx, connect_rx) = mpsc::channel();
info!("TCP worker thread started with config: {}", config_description); info!(
"TCP worker thread started with config: {}",
config_description
);
let tcp_thread = thread::spawn(move || { let tcp_thread = thread::spawn(move || {
let connect_result = panic::catch_unwind(AssertUnwindSafe(|| connect_stream(&config))); let connect_result =
panic::catch_unwind(AssertUnwindSafe(|| connect_stream(&config)));
match connect_result { match connect_result {
Ok(Ok(stream)) => { Ok(Ok(stream)) => {
@@ -184,7 +186,8 @@ impl TcpClient {
match &mut state { match &mut state {
ConnectionState::Connected => { ConnectionState::Connected => {
if let Some(stream) = connection.as_mut() { if let Some(stream) = connection.as_mut() {
if let Err(error) = send_over_stream(stream, &context, message) { if let Err(error) = send_over_stream(stream, &context, message)
{
info!("Failed to send message: {}", error); info!("Failed to send message: {}", error);
state = ConnectionState::Failed(error); state = ConnectionState::Failed(error);
connection = None; connection = None;

View File

@@ -19,6 +19,16 @@ pub fn send_marker_cot(
"Sending Marker Cursor Over Time to TCP server" "Sending Marker Cursor Over Time to TCP server"
} }
pub fn send_report_marker_cot(
ctx: Context,
cursor_over_time: cot::report_marker::ReportMarkerCoTPayload,
) -> &'static str {
let payload = cursor_over_time.to_cot().convert_to_xml();
send_payload(ctx, payload);
"Sending Report Marker Cursor Over Time to TCP server"
}
pub fn send_digital_pointer_cot( pub fn send_digital_pointer_cot(
ctx: Context, ctx: Context,
cursor_over_time: cot::digital_pointer::DigitalPointerPayload, cursor_over_time: cot::digital_pointer::DigitalPointerPayload,
@@ -29,6 +39,12 @@ pub fn send_digital_pointer_cot(
"Sending Digital Pointer Cursor Over Time to TCP server" "Sending Digital Pointer Cursor Over Time to TCP server"
} }
pub fn send_delete_cot(ctx: Context, payload: cot::delete::DeleteCoTPayload) -> &'static str {
send_payload(ctx, payload.to_xml());
"Sending Delete Cursor Over Time to TCP server"
}
pub fn send_message_cot( pub fn send_message_cot(
ctx: Context, ctx: Context,
message_payload: cot::message::MessagePayload, message_payload: cot::message::MessagePayload,
@@ -50,10 +66,7 @@ pub fn send_uas_platform_cot(
"Sending UAS Platform main CoT to TCP server" "Sending UAS Platform main CoT to TCP server"
} }
pub fn send_uas_video_cot( pub fn send_uas_video_cot(ctx: Context, payload: cot::uas::UasVideoCoTPayload) -> &'static str {
ctx: Context,
payload: cot::uas::UasVideoCoTPayload,
) -> &'static str {
let xml = payload.to_xml(); let xml = payload.to_xml();
if !xml.is_empty() { if !xml.is_empty() {
send_payload(ctx, xml); send_payload(ctx, xml);
@@ -62,10 +75,7 @@ pub fn send_uas_video_cot(
"Sending UAS Video (b-i-v) CoT to TCP server" "Sending UAS Video (b-i-v) CoT to TCP server"
} }
pub fn send_uas_sensor_cot( pub fn send_uas_sensor_cot(ctx: Context, payload: cot::uas::UasSensorCoTPayload) -> &'static str {
ctx: Context,
payload: cot::uas::UasSensorCoTPayload,
) -> &'static str {
let xml = payload.to_xml(); let xml = payload.to_xml();
send_payload(ctx, xml); send_payload(ctx, xml);

View File

@@ -1,37 +1,90 @@
use arma_rs::Context; use arma_rs::Context;
use log::info;
use crate::{cot, tcp::send_payload}; use crate::{cot, tcp::send_payload};
fn day_stale() -> (String, String) {
let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let stale = (chrono::Utc::now() + chrono::Duration::days(1))
.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
(now, stale)
}
fn payload_stale(stale_seconds: i64) -> (String, String) {
let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let stale = (chrono::Utc::now() + chrono::Duration::seconds(stale_seconds.max(1)))
.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
(now, stale)
}
pub fn send_circle_cot( pub fn send_circle_cot(
ctx: Context, ctx: Context,
circle_payload: cot::draws::circle::CircleCoTPayload, circle_payload: cot::draws::circle::CircleCoTPayload,
) -> &'static str { ) -> &'static str {
let shape_circle_cot = circle_payload.to_cot(); let shape_circle_cot = circle_payload.to_cot();
let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true); let (now, stale) = day_stale();
let stale = (chrono::Utc::now() + chrono::Duration::days(1))
.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let payload = shape_circle_cot.to_xml(&now, &stale); let payload = shape_circle_cot.to_xml(&now, &stale);
send_payload(ctx, payload); send_payload(ctx, payload);
"Sending Circle CoT to TCP server" "Sending Circle CoT to TCP server"
} }
pub fn send_ellipse_cot(ctx: Context) -> &'static str { pub fn send_ellipse_cot(
let _ = ctx; ctx: Context,
"Not implemented: send_ellipse_cot" ellipse_payload: cot::draws::shape::DrawEllipsePayload,
) -> &'static str {
let (now, stale) = payload_stale(ellipse_payload.stale_seconds);
let payload = ellipse_payload.to_xml(&now, &stale);
send_payload(ctx, payload);
"Sending Ellipse CoT to TCP server"
} }
pub fn send_rectangle_cot(ctx: Context) -> &'static str { pub fn send_rectangle_cot(
let _ = ctx; ctx: Context,
"Not implemented: send_ellipse_cot" rectangle_payload: cot::draws::shape::DrawLinksPayload,
) -> &'static str {
let (now, stale) = payload_stale(rectangle_payload.stale_seconds);
let payload = rectangle_payload.to_xml(&now, &stale);
send_payload(ctx, payload);
"Sending Rectangle CoT to TCP server"
} }
pub fn send_freedraw_cot(ctx: Context) -> &'static str { pub fn send_freedraw_cot(
let _ = ctx; ctx: Context,
"Not implemented: send_ellipse_cot" freedraw_payload: cot::draws::shape::DrawLinksPayload,
) -> &'static str {
let (now, stale) = payload_stale(freedraw_payload.stale_seconds);
let payload = freedraw_payload.to_xml(&now, &stale);
send_payload(ctx, payload);
"Sending Free Draw CoT to TCP server"
} }
pub fn send_vectordraw_cot(ctx: Context) -> &'static str { pub fn send_vectordraw_cot(
let _ = ctx; ctx: Context,
"Not implemented: send_ellipse_cot" vector_payload: cot::draws::shape::DrawLinksPayload,
) -> &'static str {
let (now, stale) = payload_stale(vector_payload.stale_seconds);
let payload = vector_payload.to_xml(&now, &stale);
send_payload(ctx, payload);
"Sending Tactical Graphic CoT to TCP server"
}
pub fn send_route_cot(
ctx: Context,
route_payload: cot::draws::route::RoutePayload,
) -> &'static str {
let (now, stale) = payload_stale(route_payload.stale_seconds);
let payload = route_payload.to_xml(&now, &stale);
info!(
"Sending ATAK route '{}' ({} bytes)",
route_payload.callsign,
payload.len()
);
send_payload(ctx, payload);
"Sending Route CoT to TCP server"
} }

View File

@@ -6,8 +6,8 @@ use std::fs::File;
use std::io::BufReader; use std::io::BufReader;
use std::io::Cursor; use std::io::Cursor;
use std::net::{SocketAddr, TcpStream, ToSocketAddrs}; use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
use std::time::Duration;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use crate::tcp::transport::TransportStream; use crate::tcp::transport::TransportStream;
@@ -65,7 +65,12 @@ fn resolve_address(address: &str) -> Result<SocketAddr, String> {
.to_socket_addrs() .to_socket_addrs()
.map_err(|e| format!("failed to resolve {}: {}", address, e))? .map_err(|e| format!("failed to resolve {}: {}", address, e))?
.next() .next()
.ok_or_else(|| format!("failed to resolve {}: no socket addresses returned", address)) .ok_or_else(|| {
format!(
"failed to resolve {}: no socket addresses returned",
address
)
})
} }
fn connect_tcp(address: &str) -> Result<TcpStream, String> { fn connect_tcp(address: &str) -> Result<TcpStream, String> {

View File

@@ -1,5 +1,5 @@
use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair, PKCS_RSA_SHA256};
use log::info; use log::info;
use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair, PKCS_RSA_SHA256};
use reqwest::blocking::Client; use reqwest::blocking::Client;
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
@@ -7,6 +7,9 @@ use uuid::Uuid;
use super::connector::connect_mtls_from_pem; use super::connector::connect_mtls_from_pem;
use crate::tcp::transport::TransportStream; use crate::tcp::transport::TransportStream;
const DEFAULT_MTLS_SERVER_PORT: &str = "8089";
const DEFAULT_ENROLL_PATH: &str = "/Marti/api/tls/signClient/v2";
#[derive(Deserialize)] #[derive(Deserialize)]
struct EnrollmentResponse { struct EnrollmentResponse {
#[serde(rename = "signedCert")] #[serde(rename = "signedCert")]
@@ -27,6 +30,23 @@ fn extract_tag_value(xml: &str, tag_name: &str) -> Option<String> {
Some(xml[start..end].trim().to_string()) Some(xml[start..end].trim().to_string())
} }
fn normalize_certificate_pem(certificate: &str) -> String {
let trimmed = certificate.trim();
if trimmed.contains("-----BEGIN CERTIFICATE-----") {
if trimmed.ends_with('\n') {
trimmed.to_string()
} else {
format!("{}\n", trimmed)
}
} else {
wrap_pem_body(
trimmed,
"-----BEGIN CERTIFICATE-----",
"-----END CERTIFICATE-----",
)
}
}
fn wrap_pem_body(base64_body: &str, begin: &str, end: &str) -> String { fn wrap_pem_body(base64_body: &str, begin: &str, end: &str) -> String {
let mut wrapped = String::new(); let mut wrapped = String::new();
let normalized = base64_body.trim().replace(['\r', '\n'], ""); let normalized = base64_body.trim().replace(['\r', '\n'], "");
@@ -89,10 +109,20 @@ fn fetch_enrollment_config(host: &str, enroll_port: &str) -> Result<EnrollmentCo
.text() .text()
.map_err(|e| format!("failed to read config response from {}: {}", url, e))?; .map_err(|e| format!("failed to read config response from {}: {}", url, e))?;
let server_port = extract_tag_value(&response_text, "serverPort") let server_port = extract_tag_value(&response_text, "serverPort").unwrap_or_else(|| {
.ok_or_else(|| "missing serverPort in /Marti/api/tls/config response".to_string())?; info!(
let enroll_path = extract_tag_value(&response_text, "enrollPath") "Enrollment config did not include serverPort; using default TAK mTLS port {}",
.ok_or_else(|| "missing enrollPath in /Marti/api/tls/config response".to_string())?; DEFAULT_MTLS_SERVER_PORT
);
DEFAULT_MTLS_SERVER_PORT.to_string()
});
let enroll_path = extract_tag_value(&response_text, "enrollPath").unwrap_or_else(|| {
info!(
"Enrollment config did not include enrollPath; using default TAK enrollment path {}",
DEFAULT_ENROLL_PATH
);
DEFAULT_ENROLL_PATH.to_string()
});
info!( info!(
"Enrollment config received: server_port={} enroll_path={}", "Enrollment config received: server_port={} enroll_path={}",
@@ -173,14 +203,11 @@ fn enroll_client_certificate(
enrollment.ca0.len() enrollment.ca0.len()
); );
let cert_pem = wrap_pem_body( let ca_cert_pem = normalize_certificate_pem(&enrollment.ca0);
&enrollment.signed_cert, let cert_pem = normalize_certificate_pem(&enrollment.signed_cert);
"-----BEGIN CERTIFICATE-----",
"-----END CERTIFICATE-----",
);
let key_pem = key_pair.serialize_pem(); let key_pem = key_pair.serialize_pem();
Ok((enrollment.ca0, cert_pem, key_pem)) Ok((ca_cert_pem, cert_pem, key_pem))
} }
pub fn enroll_and_connect( pub fn enroll_and_connect(
@@ -198,10 +225,7 @@ pub fn enroll_and_connect(
}; };
info!( info!(
"Starting enroll_and_connect for host={} enroll_port={} server_name={} client_uid={}", "Starting enroll_and_connect for host={} enroll_port={} server_name={} client_uid={}",
host, host, enroll_port, server_name, normalized_client_uid
enroll_port,
server_name,
normalized_client_uid
); );
let enrollment_config = fetch_enrollment_config(host, enroll_port)?; let enrollment_config = fetch_enrollment_config(host, enroll_port)?;
@@ -226,3 +250,52 @@ pub fn enroll_and_connect(
&client_key_pem, &client_key_pem,
) )
} }
#[cfg(test)]
mod tests {
use super::{extract_tag_value, normalize_certificate_pem, wrap_pem_body};
#[test]
fn extracts_tak_enrollment_config_tag_values() {
let xml = "<tlsConfig><serverPort>8089</serverPort><enrollPath>/Marti/api/tls/signClient/v2</enrollPath></tlsConfig>";
assert_eq!(
extract_tag_value(xml, "serverPort").as_deref(),
Some("8089")
);
assert_eq!(
extract_tag_value(xml, "enrollPath").as_deref(),
Some("/Marti/api/tls/signClient/v2")
);
}
#[test]
fn missing_config_tag_values_are_none_for_opentakserver_config() {
let xml =
"<tlsConfig><nameEntries><nameEntry name=\"OpenTAKServer\"/></nameEntries></tlsConfig>";
assert!(extract_tag_value(xml, "serverPort").is_none());
assert!(extract_tag_value(xml, "enrollPath").is_none());
}
#[test]
fn normalizes_base64_certificate_body_to_pem() {
let pem = normalize_certificate_pem("QUJDREVGRw==");
assert_eq!(
pem,
wrap_pem_body(
"QUJDREVGRw==",
"-----BEGIN CERTIFICATE-----",
"-----END CERTIFICATE-----"
)
);
}
#[test]
fn preserves_existing_certificate_pem() {
let pem = "-----BEGIN CERTIFICATE-----\nQUJDREVGRw==\n-----END CERTIFICATE-----\n";
assert_eq!(normalize_certificate_pem(pem), pem);
}
}

View File

@@ -35,7 +35,12 @@ fn connect_plain(address: &str) -> Result<TransportStream, String> {
.to_socket_addrs() .to_socket_addrs()
.map_err(|e| format!("failed to resolve {}: {}", address, e))? .map_err(|e| format!("failed to resolve {}: {}", address, e))?
.next() .next()
.ok_or_else(|| format!("failed to resolve {}: no socket addresses returned", address))?; .ok_or_else(|| {
format!(
"failed to resolve {}: no socket addresses returned",
address
)
})?;
info!( info!(
"Opening plain TCP connection to {} (resolved={}) with timeout {:?}", "Opening plain TCP connection to {} (resolved={}) with timeout {:?}",

View File

@@ -3,8 +3,38 @@ pub(crate) struct MavlinkCallbackEvent {
pub data: String, pub data: String,
} }
use super::constants::{AUTOPILOT_COMPONENT_ID, CAMERA_COMPONENT_ID, TURRET_CAMERA_COMPONENT_ID};
use super::identity::should_send_video_stream_information;
use super::packets::{
autopilot_version_packet, camera_fov_status_packet_for_component,
camera_information_packet_for_component, command_ack_packet, gimbal_manager_information_packet,
home_position_packet, mission_ack_packet, mission_request_int_packet,
mount_orientation_packet_for_component, mount_status_packet,
video_stream_information_packet_for_component, video_stream_status_packet_for_component,
};
use super::state::{latest_system, set_active_camera, set_home};
use log::info;
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
#[derive(Clone, Copy)]
struct MissionUploadState {
count: u16,
next_seq: u16,
mission_type: u8,
gcs_system: u8,
gcs_component: u8,
}
static MISSION_UPLOADS: OnceLock<Mutex<HashMap<u8, MissionUploadState>>> = OnceLock::new();
fn mission_uploads() -> &'static Mutex<HashMap<u8, MissionUploadState>> {
MISSION_UPLOADS.get_or_init(|| Mutex::new(HashMap::new()))
}
pub(crate) fn hex_preview(bytes: &[u8], max_len: usize) -> String { pub(crate) fn hex_preview(bytes: &[u8], max_len: usize) -> String {
bytes.iter() bytes
.iter()
.take(max_len) .take(max_len)
.map(|byte| format!("{:02X}", byte)) .map(|byte| format!("{:02X}", byte))
.collect::<Vec<_>>() .collect::<Vec<_>>()
@@ -13,19 +43,42 @@ pub(crate) fn hex_preview(bytes: &[u8], max_len: usize) -> String {
pub(crate) fn mav_cmd_name(command_id: u16) -> &'static str { pub(crate) fn mav_cmd_name(command_id: u16) -> &'static str {
match command_id { match command_id {
16 => "NAV_WAYPOINT",
17 => "NAV_LOITER_UNLIM",
20 => "NAV_RETURN_TO_LAUNCH",
21 => "NAV_LAND",
22 => "NAV_TAKEOFF", 22 => "NAV_TAKEOFF",
176 => "DO_SET_MODE", 176 => "DO_SET_MODE",
178 => "DO_CHANGE_SPEED",
179 => "DO_SET_HOME",
192 => "DO_REPOSITION",
205 => "DO_MOUNT_CONTROL",
43000 => "GUIDED_CHANGE_SPEED",
43001 => "GUIDED_CHANGE_ALTITUDE",
43002 => "GUIDED_CHANGE_HEADING",
200 => "IMAGE_START_CAPTURE", 200 => "IMAGE_START_CAPTURE",
201 => "IMAGE_STOP_CAPTURE", 201 => "IMAGE_STOP_CAPTURE",
250 => "VIDEO_START_CAPTURE", 250 => "VIDEO_START_CAPTURE",
251 => "VIDEO_STOP_CAPTURE", 251 => "VIDEO_STOP_CAPTURE",
252 => "DO_CONTROL_VIDEO", 252 => "DO_CONTROL_VIDEO",
400 => "COMPONENT_ARM_DISARM", 400 => "COMPONENT_ARM_DISARM",
521 => "REQUEST_MESSAGE", 511 => "SET_MESSAGE_INTERVAL",
512 => "REQUEST_MESSAGE",
521 => "REQUEST_CAMERA_INFORMATION",
2502 => "VIDEO_START_STREAMING",
2503 => "VIDEO_STOP_STREAMING",
2504 => "REQUEST_VIDEO_STREAM_INFORMATION",
2505 => "REQUEST_VIDEO_STREAM_STATUS",
_ => "UNKNOWN", _ => "UNKNOWN",
} }
} }
fn read_i32(payload: &[u8], offset: usize) -> Option<i32> {
payload
.get(offset..offset + 4)
.map(|bytes| i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
fn read_i16(payload: &[u8], offset: usize) -> Option<i16> { fn read_i16(payload: &[u8], offset: usize) -> Option<i16> {
payload payload
.get(offset..offset + 2) .get(offset..offset + 2)
@@ -46,10 +99,10 @@ fn read_f32(payload: &[u8], offset: usize) -> Option<f32> {
fn mavlink_message_detail(msg_id: u8, payload: &[u8]) -> String { fn mavlink_message_detail(msg_id: u8, payload: &[u8]) -> String {
match msg_id { match msg_id {
76 if payload.len() >= 33 => { 76 if payload.len() >= 31 => {
let command = u16::from_le_bytes([payload[28], payload[29]]); let command = u16::from_le_bytes([payload[28], payload[29]]);
let target_system = payload[30]; let target_system = payload[30];
let target_component = payload[31]; let target_component = payload.get(31).copied().unwrap_or(0);
format!( format!(
" command={}({}) target={}:{}", " command={}({}) target={}:{}",
command, command,
@@ -86,7 +139,8 @@ pub(crate) fn mavlink_packet_summary(bytes: &[u8]) -> String {
let system_id = bytes[3]; let system_id = bytes[3];
let component_id = bytes[4]; let component_id = bytes[4];
let msg_id = bytes[5]; let msg_id = bytes[5];
let detail = mavlink_message_detail(msg_id, bytes.get(6..6 + payload_len).unwrap_or(&[])); let detail =
mavlink_message_detail(msg_id, bytes.get(6..6 + payload_len).unwrap_or(&[]));
format!( format!(
"MAVLink v1 msgid={}{} sysid={} compid={} seq={} payload_len={} preview={}", "MAVLink v1 msgid={}{} sysid={} compid={} seq={} payload_len={} preview={}",
msg_id, msg_id,
@@ -104,7 +158,10 @@ pub(crate) fn mavlink_packet_summary(bytes: &[u8]) -> String {
let system_id = bytes[5]; let system_id = bytes[5];
let component_id = bytes[6]; let component_id = bytes[6];
let msg_id = bytes[7] as u32 | ((bytes[8] as u32) << 8) | ((bytes[9] as u32) << 16); let msg_id = bytes[7] as u32 | ((bytes[8] as u32) << 8) | ((bytes[9] as u32) << 16);
let detail = mavlink_message_detail(msg_id as u8, bytes.get(10..10 + payload_len).unwrap_or(&[])); let detail = mavlink_message_detail(
msg_id as u8,
bytes.get(10..10 + payload_len).unwrap_or(&[]),
);
format!( format!(
"MAVLink v2 msgid={}{} sysid={} compid={} seq={} payload_len={} preview={}", "MAVLink v2 msgid={}{} sysid={} compid={} seq={} payload_len={} preview={}",
msg_id, msg_id,
@@ -163,11 +220,11 @@ pub(crate) fn mavlink_callback_event(bytes: &[u8], source: &str) -> Option<Mavli
), ),
}) })
} }
76 if payload.len() >= 33 => { 76 if payload.len() >= 31 => {
let command = read_u16(payload, 28)?; let command = read_u16(payload, 28)?;
let target_system = *payload.get(30)?; let target_system = *payload.get(30)?;
let target_component = *payload.get(31)?; let target_component = payload.get(31).copied().unwrap_or(0);
let confirmation = *payload.get(32)?; let confirmation = payload.get(32).copied().unwrap_or(0);
Some(MavlinkCallbackEvent { Some(MavlinkCallbackEvent {
function: "COMMAND_LONG", function: "COMMAND_LONG",
data: format!( data: format!(
@@ -200,7 +257,7 @@ pub(crate) fn mavlink_callback_event(bytes: &[u8], source: &str) -> Option<Mavli
Some(MavlinkCallbackEvent { Some(MavlinkCallbackEvent {
function: "COMMAND_INT", function: "COMMAND_INT",
data: format!( data: format!(
"source={};sysid={};compid={};command={};command_name={};target_system={};target_component={};frame={};current={};autocontinue={};param1={:.3};param2={:.3};param3={:.3};param4={:.3}", "source={};sysid={};compid={};command={};command_name={};target_system={};target_component={};frame={};current={};autocontinue={};param1={:.3};param2={:.3};param3={:.3};param4={:.3};x={};y={};z={:.3}",
source, source,
system_id, system_id,
component_id, component_id,
@@ -215,6 +272,181 @@ pub(crate) fn mavlink_callback_event(bytes: &[u8], source: &str) -> Option<Mavli
read_f32(payload, 4).unwrap_or(0.0), read_f32(payload, 4).unwrap_or(0.0),
read_f32(payload, 8).unwrap_or(0.0), read_f32(payload, 8).unwrap_or(0.0),
read_f32(payload, 12).unwrap_or(0.0), read_f32(payload, 12).unwrap_or(0.0),
read_i32(payload, 16).unwrap_or(0),
read_i32(payload, 20).unwrap_or(0),
read_f32(payload, 24).unwrap_or(0.0),
),
})
}
44 if payload.len() >= 4 => {
let count = read_u16(payload, 0)?;
let target_system = *payload.get(2)?;
let target_component = *payload.get(3)?;
let mission_type = payload.get(4).copied().unwrap_or(0);
Some(MavlinkCallbackEvent {
function: "MISSION_COUNT",
data: format!(
"source={};sysid={};compid={};target_system={};target_component={};count={};mission_type={}",
source, system_id, component_id, target_system, target_component, count, mission_type
),
})
}
39 if payload.len() >= 37 => {
let seq = read_u16(payload, 28)?;
let command = read_u16(payload, 30)?;
let target_system = *payload.get(32)?;
let target_component = *payload.get(33)?;
let frame = *payload.get(34)?;
let current = *payload.get(35)?;
let autocontinue = *payload.get(36)?;
let mission_type = payload.get(37).copied().unwrap_or(0);
Some(MavlinkCallbackEvent {
function: "MISSION_ITEM",
data: format!(
"source={};sysid={};compid={};seq={};command={};command_name={};target_system={};target_component={};frame={};current={};autocontinue={};mission_type={};param1={:.3};param2={:.3};param3={:.3};param4={:.3};lat={:.7};lon={:.7};alt={:.3}",
source,
system_id,
component_id,
seq,
command,
mav_cmd_name(command),
target_system,
target_component,
frame,
current,
autocontinue,
mission_type,
read_f32(payload, 0).unwrap_or(0.0),
read_f32(payload, 4).unwrap_or(0.0),
read_f32(payload, 8).unwrap_or(0.0),
read_f32(payload, 12).unwrap_or(0.0),
read_f32(payload, 16).unwrap_or(0.0),
read_f32(payload, 20).unwrap_or(0.0),
read_f32(payload, 24).unwrap_or(0.0),
),
})
}
73 if payload.len() >= 37 => {
let seq = read_u16(payload, 28)?;
let command = read_u16(payload, 30)?;
let target_system = *payload.get(32)?;
let target_component = *payload.get(33)?;
let frame = *payload.get(34)?;
let current = *payload.get(35)?;
let autocontinue = *payload.get(36)?;
let mission_type = payload.get(37).copied().unwrap_or(0);
Some(MavlinkCallbackEvent {
function: "MISSION_ITEM_INT",
data: format!(
"source={};sysid={};compid={};seq={};command={};command_name={};target_system={};target_component={};frame={};current={};autocontinue={};mission_type={};param1={:.3};param2={:.3};param3={:.3};param4={:.3};x={};y={};z={:.3};lat={:.7};lon={:.7};alt={:.3}",
source,
system_id,
component_id,
seq,
command,
mav_cmd_name(command),
target_system,
target_component,
frame,
current,
autocontinue,
mission_type,
read_f32(payload, 0).unwrap_or(0.0),
read_f32(payload, 4).unwrap_or(0.0),
read_f32(payload, 8).unwrap_or(0.0),
read_f32(payload, 12).unwrap_or(0.0),
read_i32(payload, 16).unwrap_or(0),
read_i32(payload, 20).unwrap_or(0),
read_f32(payload, 24).unwrap_or(0.0),
read_i32(payload, 16).unwrap_or(0) as f64 / 1e7,
read_i32(payload, 20).unwrap_or(0) as f64 / 1e7,
read_f32(payload, 24).unwrap_or(0.0),
),
})
}
45 if payload.len() >= 2 => {
let target_system = *payload.get(0)?;
let target_component = *payload.get(1)?;
let mission_type = payload.get(2).copied().unwrap_or(0);
Some(MavlinkCallbackEvent {
function: "MISSION_CLEAR_ALL",
data: format!(
"source={};sysid={};compid={};target_system={};target_component={};mission_type={}",
source, system_id, component_id, target_system, target_component, mission_type
),
})
}
41 if payload.len() >= 4 => {
let seq = read_u16(payload, 0)?;
let target_system = *payload.get(2)?;
let target_component = *payload.get(3)?;
Some(MavlinkCallbackEvent {
function: "MISSION_SET_CURRENT",
data: format!(
"source={};sysid={};compid={};target_system={};target_component={};seq={}",
source, system_id, component_id, target_system, target_component, seq
),
})
}
243 if payload.len() >= 53 => {
let lat_int = read_i32(payload, 0)?;
let lon_int = read_i32(payload, 4)?;
let alt_mm = read_i32(payload, 8)?;
let target_system = *payload.get(52)?;
Some(MavlinkCallbackEvent {
function: "SET_HOME_POSITION",
data: format!(
"source={};sysid={};compid={};target_system={};lat={:.7};lon={:.7};alt={:.3}",
source,
system_id,
component_id,
target_system,
lat_int as f64 / 1e7,
lon_int as f64 / 1e7,
alt_mm as f32 / 1000.0,
),
})
}
86 if payload.len() >= 53 => {
let lat_int = read_i32(payload, 4)?;
let lon_int = read_i32(payload, 8)?;
let alt = read_f32(payload, 12)?;
let type_mask = read_u16(payload, 48)?;
let target_system = *payload.get(50)?;
let target_component = *payload.get(51)?;
let coordinate_frame = *payload.get(52)?;
Some(MavlinkCallbackEvent {
function: "SET_POSITION_TARGET_GLOBAL_INT",
data: format!(
"source={};sysid={};compid={};target_system={};target_component={};coordinate_frame={};type_mask={};lat={:.7};lon={:.7};alt={:.3};vx={:.3};vy={:.3};vz={:.3};yaw={:.3}",
source,
system_id,
component_id,
target_system,
target_component,
coordinate_frame,
type_mask,
lat_int as f64 / 1e7,
lon_int as f64 / 1e7,
alt,
read_f32(payload, 16).unwrap_or(0.0),
read_f32(payload, 20).unwrap_or(0.0),
read_f32(payload, 24).unwrap_or(0.0),
read_f32(payload, 40).unwrap_or(0.0),
),
})
}
11 if payload.len() >= 6 => {
let custom_mode = payload
.get(0..4)
.map(|bytes| u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))?;
let target_system = *payload.get(4)?;
let base_mode = *payload.get(5)?;
Some(MavlinkCallbackEvent {
function: "SET_MODE",
data: format!(
"source={};sysid={};compid={};target_system={};base_mode={};custom_mode={}",
source, system_id, component_id, target_system, base_mode, custom_mode
), ),
}) })
} }
@@ -237,3 +469,553 @@ pub(crate) fn mavlink_callback_event(bytes: &[u8], source: &str) -> Option<Mavli
_ => None, _ => None,
} }
} }
pub(crate) fn mavlink_response_packets(bytes: &[u8]) -> Vec<Vec<u8>> {
if bytes.len() < 8 {
return Vec::new();
}
let (msg_id, system_id, component_id, payload) = match bytes[0] {
0xFE if bytes.len() >= 8 => {
let payload_len = bytes[1] as usize;
(
bytes[5] as u32,
bytes[3],
bytes[4],
bytes.get(6..6 + payload_len).unwrap_or(&[]),
)
}
0xFD if bytes.len() >= 12 => {
let payload_len = bytes[1] as usize;
(
bytes[7] as u32 | ((bytes[8] as u32) << 8) | ((bytes[9] as u32) << 16),
bytes[5],
bytes[6],
bytes.get(10..10 + payload_len).unwrap_or(&[]),
)
}
_ => return Vec::new(),
};
match msg_id {
76 if payload.len() >= 31 => command_long_response_packets(payload),
75 if payload.len() >= 35 => command_int_response_packets(payload),
44 if payload.len() >= 4 => {
mission_count_response_packets(system_id, component_id, payload)
}
39 | 73 if payload.len() >= 37 => mission_item_response_packets(payload),
45 if payload.len() >= 2 => mission_clear_all_response_packets(payload),
243 if payload.len() >= 53 => set_home_position_response_packets(payload),
_ => Vec::new(),
}
}
fn command_long_response_packets(payload: &[u8]) -> Vec<Vec<u8>> {
let command = match read_u16(payload, 28) {
Some(command) => command,
None => return Vec::new(),
};
let target_system = *payload.get(30).unwrap_or(&0);
let target_component = *payload.get(31).unwrap_or(&0);
if target_system == 0 {
return Vec::new();
}
let ack_component = if target_component == 0 {
AUTOPILOT_COMPONENT_ID
} else {
target_component
};
let mut packets = vec![command_ack_packet(target_system, ack_component, command, 0)];
match command {
512 => {
let requested_message = read_f32(payload, 0).unwrap_or(0.0).round() as u32;
packets.extend(requested_message_packets(target_system, requested_message));
}
179 => {
if target_system != 0 {
let use_current = read_f32(payload, 0).unwrap_or(0.0) >= 1.0;
let lat = read_f32(payload, 16).unwrap_or(0.0) as f64;
let lon = read_f32(payload, 20).unwrap_or(0.0) as f64;
let alt = read_f32(payload, 24).unwrap_or(0.0);
if use_current {
if let Some(system) = latest_system(target_system) {
set_home(
target_system,
system.lat_deg,
system.lon_deg,
system.alt_msl_m - system.rel_alt_m,
);
}
} else if lat != 0.0 || lon != 0.0 {
set_home(target_system, lat, lon, alt);
}
}
}
521 => {
if let Some(system) = latest_system(target_system) {
let camera_component =
camera_component_for_target(target_component, system.has_turret_camera);
set_active_camera(target_system, camera_component);
info!(
"MAVLink camera selection command={} target_system={} target_component={} resolved_camera_component={}",
command, target_system, target_component, camera_component
);
packets.push(camera_information_packet_for_component(
target_system,
camera_component,
&camera_name(&system.callsign, camera_component, system.has_turret_camera),
gimbal_device_for_target(target_component, system.has_turret_camera),
));
}
}
2502 | 2503 | 2505 => {
if let Some(system) = latest_system(target_system) {
let camera_component =
camera_component_for_target(target_component, system.has_turret_camera);
set_active_camera(target_system, camera_component);
info!(
"MAVLink camera selection command={} target_system={} target_component={} resolved_camera_component={}",
command, target_system, target_component, camera_component
);
packets.push(video_stream_status_packet_for_component(
target_system,
camera_component,
system.hfov_deg,
1,
false,
));
}
}
2504 => {
if let Some(system) = latest_system(target_system) {
if should_send_video_stream_information(&system.video_uri) {
let camera_component =
camera_component_for_target(target_component, system.has_turret_camera);
set_active_camera(target_system, camera_component);
info!(
"MAVLink camera selection command={} target_system={} target_component={} resolved_camera_component={}",
command, target_system, target_component, camera_component
);
packets.push(video_stream_information_packet_for_component(
target_system,
camera_component,
&camera_name(&system.callsign, camera_component, system.has_turret_camera),
&system.video_uri,
system.hfov_deg,
1,
1,
false,
));
}
}
}
_ => {}
}
packets
}
fn command_int_response_packets(payload: &[u8]) -> Vec<Vec<u8>> {
let command = match read_u16(payload, 28) {
Some(command) => command,
None => return Vec::new(),
};
let target_system = *payload.get(30).unwrap_or(&0);
let target_component = *payload.get(31).unwrap_or(&AUTOPILOT_COMPONENT_ID);
if target_system == 0 {
return Vec::new();
}
vec![command_ack_packet(
target_system,
if target_component == 0 {
AUTOPILOT_COMPONENT_ID
} else {
target_component
},
command,
0,
)]
}
fn mission_count_response_packets(
gcs_system: u8,
gcs_component: u8,
payload: &[u8],
) -> Vec<Vec<u8>> {
let count = match read_u16(payload, 0) {
Some(count) => count,
None => return Vec::new(),
};
let target_system = *payload.get(2).unwrap_or(&0);
let target_component = *payload.get(3).unwrap_or(&AUTOPILOT_COMPONENT_ID);
let mission_type = payload.get(4).copied().unwrap_or(0);
if target_system == 0 {
return Vec::new();
}
let ack_component = if target_component == 0 {
AUTOPILOT_COMPONENT_ID
} else {
target_component
};
let Ok(mut uploads) = mission_uploads().lock() else {
return Vec::new();
};
if count == 0 {
uploads.remove(&target_system);
return vec![mission_ack_packet(
target_system,
ack_component,
gcs_system,
gcs_component,
mission_type,
)];
}
uploads.insert(
target_system,
MissionUploadState {
count,
next_seq: 0,
mission_type,
gcs_system,
gcs_component,
},
);
vec![mission_request_int_packet(
target_system,
ack_component,
gcs_system,
gcs_component,
0,
mission_type,
)]
}
fn mission_item_response_packets(payload: &[u8]) -> Vec<Vec<u8>> {
let seq = match read_u16(payload, 28) {
Some(seq) => seq,
None => return Vec::new(),
};
let target_system = *payload.get(32).unwrap_or(&0);
let target_component = *payload.get(33).unwrap_or(&AUTOPILOT_COMPONENT_ID);
if target_system == 0 {
return Vec::new();
}
let ack_component = if target_component == 0 {
AUTOPILOT_COMPONENT_ID
} else {
target_component
};
let Ok(mut uploads) = mission_uploads().lock() else {
return Vec::new();
};
let Some(state) = uploads.get_mut(&target_system) else {
return vec![mission_ack_packet(
target_system,
ack_component,
255,
190,
payload.get(37).copied().unwrap_or(0),
)];
};
state.next_seq = seq.saturating_add(1);
if state.next_seq < state.count {
vec![mission_request_int_packet(
target_system,
ack_component,
state.gcs_system,
state.gcs_component,
state.next_seq,
state.mission_type,
)]
} else {
let ack = mission_ack_packet(
target_system,
ack_component,
state.gcs_system,
state.gcs_component,
state.mission_type,
);
uploads.remove(&target_system);
vec![ack]
}
}
fn mission_clear_all_response_packets(payload: &[u8]) -> Vec<Vec<u8>> {
let target_system = *payload.get(0).unwrap_or(&0);
let target_component = *payload.get(1).unwrap_or(&AUTOPILOT_COMPONENT_ID);
if target_system == 0 {
return Vec::new();
}
if let Ok(mut uploads) = mission_uploads().lock() {
uploads.remove(&target_system);
}
vec![mission_ack_packet(
target_system,
if target_component == 0 {
AUTOPILOT_COMPONENT_ID
} else {
target_component
},
255,
190,
payload.get(2).copied().unwrap_or(0),
)]
}
fn set_home_position_response_packets(payload: &[u8]) -> Vec<Vec<u8>> {
let target_system = *payload.get(52).unwrap_or(&0);
if target_system == 0 {
return Vec::new();
}
let lat = read_i32(payload, 0).unwrap_or(0) as f64 / 1e7;
let lon = read_i32(payload, 4).unwrap_or(0) as f64 / 1e7;
let alt = read_i32(payload, 8).unwrap_or(0) as f32 / 1000.0;
if lat != 0.0 || lon != 0.0 {
set_home(target_system, lat, lon, alt);
}
Vec::new()
}
fn requested_message_packets(system_id: u8, requested_message: u32) -> Vec<Vec<u8>> {
let Some(system) = latest_system(system_id) else {
return Vec::new();
};
match requested_message {
148 => vec![autopilot_version_packet(
system_id,
&system.mavlink_identity,
)],
242 => vec![home_position_packet(
system_id,
system.home_lat_deg,
system.home_lon_deg,
system.home_alt_msl_m,
system.heading_deg,
)],
259 => {
let mut packets = vec![camera_information_packet_for_component(
system_id,
CAMERA_COMPONENT_ID,
&camera_name(
&system.callsign,
CAMERA_COMPONENT_ID,
system.has_turret_camera,
),
0,
)];
if system.has_turret_camera {
packets.push(camera_information_packet_for_component(
system_id,
TURRET_CAMERA_COMPONENT_ID,
&camera_name(&system.callsign, TURRET_CAMERA_COMPONENT_ID, true),
super::constants::GIMBAL_COMPONENT_ID,
));
}
packets
}
269 => {
if should_send_video_stream_information(&system.video_uri) {
let mut packets = vec![video_stream_information_packet_for_component(
system_id,
CAMERA_COMPONENT_ID,
&camera_name(
&system.callsign,
CAMERA_COMPONENT_ID,
system.has_turret_camera,
),
&system.video_uri,
system.hfov_deg,
1,
1,
false,
)];
if system.has_turret_camera {
packets.push(video_stream_information_packet_for_component(
system_id,
TURRET_CAMERA_COMPONENT_ID,
&camera_name(&system.callsign, TURRET_CAMERA_COMPONENT_ID, true),
&system.video_uri,
system.hfov_deg,
1,
1,
false,
));
}
packets
} else {
Vec::new()
}
}
270 => {
let mut packets = vec![video_stream_status_packet_for_component(
system_id,
CAMERA_COMPONENT_ID,
system.hfov_deg,
1,
false,
)];
if system.has_turret_camera {
packets.push(video_stream_status_packet_for_component(
system_id,
TURRET_CAMERA_COMPONENT_ID,
system.hfov_deg,
1,
false,
));
}
packets
}
265 => {
let mut packets = vec![mount_orientation_packet_for_component(
system_id,
CAMERA_COMPONENT_ID,
system.fpv_pitch_deg,
system.fpv_yaw_deg,
)];
if system.has_turret_camera {
packets.push(mount_orientation_packet_for_component(
system_id,
TURRET_CAMERA_COMPONENT_ID,
system.gimbal_pitch_deg,
system.gimbal_yaw_deg,
));
}
packets
}
271 => {
let (fpv_image_lat, fpv_image_lon, fpv_image_alt) = fpv_image_point(
system.lat_deg,
system.lon_deg,
system.alt_msl_m,
system.rel_alt_m,
system.fpv_pitch_deg,
system.fpv_yaw_deg,
);
let mut packets = vec![camera_fov_status_packet_for_component(
system_id,
CAMERA_COMPONENT_ID,
system.lat_deg,
system.lon_deg,
system.alt_msl_m,
fpv_image_lat,
fpv_image_lon,
fpv_image_alt,
0.0,
system.fpv_pitch_deg,
system.fpv_yaw_deg,
system.hfov_deg,
system.vfov_deg,
)];
if system.has_turret_camera {
packets.push(camera_fov_status_packet_for_component(
system_id,
TURRET_CAMERA_COMPONENT_ID,
system.lat_deg,
system.lon_deg,
system.alt_msl_m,
system.image_lat_deg,
system.image_lon_deg,
system.image_alt_msl_m,
0.0,
system.gimbal_pitch_deg,
system.gimbal_yaw_deg,
system.hfov_deg,
system.vfov_deg,
));
}
packets
}
158 => {
let active_component = if system.has_turret_camera {
system.active_camera_component
} else {
CAMERA_COMPONENT_ID
};
let (pitch, roll, relative_yaw) = if active_component == TURRET_CAMERA_COMPONENT_ID {
(
system.gimbal_pitch_deg,
0.0,
normalize_signed_deg(system.gimbal_yaw_deg - system.fpv_yaw_deg),
)
} else {
(system.fpv_pitch_deg, 0.0, 0.0)
};
vec![mount_status_packet(system_id, pitch, roll, relative_yaw)]
}
280 => vec![gimbal_manager_information_packet(system_id)],
_ => Vec::new(),
}
}
fn camera_component_for_target(target_component: u8, has_turret_camera: bool) -> u8 {
if has_turret_camera && target_component == TURRET_CAMERA_COMPONENT_ID {
TURRET_CAMERA_COMPONENT_ID
} else {
CAMERA_COMPONENT_ID
}
}
fn gimbal_device_for_target(target_component: u8, has_turret_camera: bool) -> u8 {
if has_turret_camera && target_component == TURRET_CAMERA_COMPONENT_ID {
super::constants::GIMBAL_COMPONENT_ID
} else {
0
}
}
fn camera_name(callsign: &str, component_id: u8, has_turret_camera: bool) -> String {
if has_turret_camera && component_id == TURRET_CAMERA_COMPONENT_ID {
format!("{callsign} Turret")
} else {
format!("{callsign} FPV")
}
}
fn normalize_signed_deg(value: f32) -> f32 {
let normalized = ((value % 360.0) + 360.0) % 360.0;
if normalized > 180.0 {
normalized - 360.0
} else {
normalized
}
}
fn fpv_image_point(
lat_deg: f64,
lon_deg: f64,
alt_msl_m: f32,
rel_alt_m: f32,
pitch_deg: f32,
yaw_deg: f32,
) -> (f64, f64, f32) {
let pitch_rad = pitch_deg.to_radians();
let vertical = (-pitch_rad.sin()).max(0.01);
let slant_m = (rel_alt_m.max(1.0) / vertical).clamp(1.0, 15_000.0);
let ground_m = slant_m * pitch_rad.cos().abs();
let yaw_rad = yaw_deg.to_radians();
let north_m = ground_m * yaw_rad.cos();
let east_m = ground_m * yaw_rad.sin();
let lat_rad = lat_deg.to_radians();
let meters_per_degree_lat = 111_320.0;
let meters_per_degree_lon = (111_320.0 * lat_rad.cos().abs()).max(1.0);
(
lat_deg + north_m as f64 / meters_per_degree_lat,
lon_deg + east_m as f64 / meters_per_degree_lon,
alt_msl_m - rel_alt_m,
)
}

View File

@@ -1,5 +1,6 @@
pub const AUTOPILOT_COMPONENT_ID: u8 = 1; pub const AUTOPILOT_COMPONENT_ID: u8 = 1;
pub const CAMERA_COMPONENT_ID: u8 = 100; pub const CAMERA_COMPONENT_ID: u8 = 100;
pub const TURRET_CAMERA_COMPONENT_ID: u8 = 101;
pub const GIMBAL_COMPONENT_ID: u8 = 154; pub const GIMBAL_COMPONENT_ID: u8 = 154;
pub const MAV_TYPE_FIXED_WING: u8 = 1; pub const MAV_TYPE_FIXED_WING: u8 = 1;
@@ -8,7 +9,7 @@ pub const MAV_TYPE_HELICOPTER: u8 = 4;
pub const MAV_TYPE_GIMBAL: u8 = 26; pub const MAV_TYPE_GIMBAL: u8 = 26;
pub const MAV_TYPE_CAMERA: u8 = 30; pub const MAV_TYPE_CAMERA: u8 = 30;
pub const MAV_AUTOPILOT_GENERIC: u8 = 0; pub const MAV_AUTOPILOT_ARDUPILOTMEGA: u8 = 3;
pub const MAV_AUTOPILOT_INVALID: u8 = 8; pub const MAV_AUTOPILOT_INVALID: u8 = 8;
pub const MAV_STATE_STANDBY: u8 = 3; pub const MAV_STATE_STANDBY: u8 = 3;
@@ -27,6 +28,10 @@ pub const MAV_PROTOCOL_CAPABILITY_SET_POSITION_TARGET_GLOBAL_INT: u64 = 256;
pub const MAV_PROTOCOL_CAPABILITY_MAVLINK2: u64 = 8192; pub const MAV_PROTOCOL_CAPABILITY_MAVLINK2: u64 = 8192;
pub const MAV_PROTOCOL_CAPABILITY_COMPONENT_IMPLEMENTS_GIMBAL_MANAGER: u64 = 262_144; pub const MAV_PROTOCOL_CAPABILITY_COMPONENT_IMPLEMENTS_GIMBAL_MANAGER: u64 = 262_144;
pub const CAMERA_CAP_FLAGS_CAPTURE_VIDEO: u32 = 1;
pub const CAMERA_CAP_FLAGS_CAPTURE_IMAGE: u32 = 2;
pub const CAMERA_CAP_FLAGS_HAS_MODES: u32 = 4;
pub const CAMERA_CAP_FLAGS_HAS_BASIC_ZOOM: u32 = 64;
pub const CAMERA_CAP_FLAGS_HAS_VIDEO_STREAM: u32 = 256; pub const CAMERA_CAP_FLAGS_HAS_VIDEO_STREAM: u32 = 256;
pub const VIDEO_STREAM_STATUS_FLAGS_RUNNING: u16 = 1; pub const VIDEO_STREAM_STATUS_FLAGS_RUNNING: u16 = 1;
pub const VIDEO_STREAM_TYPE_RTSP: u8 = 0; pub const VIDEO_STREAM_TYPE_RTSP: u8 = 0;
@@ -34,3 +39,5 @@ pub const VIDEO_STREAM_TYPE_RTPUDP: u8 = 1;
pub const VIDEO_STREAM_TYPE_TCP_MPEG: u8 = 2; pub const VIDEO_STREAM_TYPE_TCP_MPEG: u8 = 2;
pub const VIDEO_STREAM_TYPE_MPEG_TS: u8 = 3; pub const VIDEO_STREAM_TYPE_MPEG_TS: u8 = 3;
pub const VIDEO_STREAM_ENCODING_H264: u8 = 1; pub const VIDEO_STREAM_ENCODING_H264: u8 = 1;
pub const GIMBAL_MANAGER_CAP_FLAGS_BASIC_PITCH_YAW: u32 = 32 | 128 | 256 | 1024 | 4096 | 131_072;

View File

@@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle}; use std::thread::{self, JoinHandle};
use std::time::Duration; use std::time::Duration;
use super::callbacks::{mavlink_callback_event, mavlink_packet_summary}; use super::callbacks::{mavlink_callback_event, mavlink_packet_summary, mavlink_response_packets};
pub(crate) struct MavlinkEndpoint { pub(crate) struct MavlinkEndpoint {
pub socket: UdpSocket, pub socket: UdpSocket,
@@ -18,10 +18,11 @@ pub(crate) struct MavlinkEndpoint {
static MAVLINK_ENDPOINT: Mutex<Option<MavlinkEndpoint>> = Mutex::new(None); static MAVLINK_ENDPOINT: Mutex<Option<MavlinkEndpoint>> = Mutex::new(None);
pub(crate) fn socket_for_send() -> Option<UdpSocket> { pub(crate) fn socket_for_send() -> Option<UdpSocket> {
MAVLINK_ENDPOINT MAVLINK_ENDPOINT.lock().ok().and_then(|endpoint| {
.lock() endpoint
.ok() .as_ref()
.and_then(|endpoint| endpoint.as_ref().and_then(|entry| entry.socket.try_clone().ok())) .and_then(|entry| entry.socket.try_clone().ok())
})
} }
fn stop_endpoint_internal() { fn stop_endpoint_internal() {
@@ -102,14 +103,27 @@ pub fn start_endpoint(ctx: Context, bind_port: i32) -> &'static str {
source, source,
mavlink_packet_summary(&buffer[..received]) mavlink_packet_summary(&buffer[..received])
); );
if let Some(event) = mavlink_callback_event(&buffer[..received], &source_string) { if let Some(event) = mavlink_callback_event(&buffer[..received], &source_string)
let _ = listener_ctx.callback_data("MAVLINK UDP", event.function, event.data); {
let _ =
listener_ctx.callback_data("MAVLINK UDP", event.function, event.data);
}
for packet in mavlink_response_packets(&buffer[..received]) {
if let Err(error) = listener_socket.send_to(&packet, source) {
info!(
"MAVLink UDP endpoint failed sending response to {}: {}",
source, error
);
break;
}
} }
} }
Err(error) Err(error)
if matches!( if matches!(
error.kind(), error.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut std::io::ErrorKind::WouldBlock
| std::io::ErrorKind::TimedOut
| std::io::ErrorKind::ConnectionReset
) => {} ) => {}
Err(error) => { Err(error) => {
if listener_running.load(Ordering::Relaxed) { if listener_running.load(Ordering::Relaxed) {

View File

@@ -1,6 +1,4 @@
use super::constants::{ use super::constants::{MAV_TYPE_FIXED_WING, MAV_TYPE_HELICOPTER, MAV_TYPE_QUADROTOR};
MAV_TYPE_FIXED_WING, MAV_TYPE_HELICOPTER, MAV_TYPE_QUADROTOR,
};
pub(crate) fn map_vehicle_type(vehicle_type: u8) -> u8 { pub(crate) fn map_vehicle_type(vehicle_type: u8) -> u8 {
match vehicle_type { match vehicle_type {
@@ -25,6 +23,23 @@ pub(crate) fn stable_system_id(entity_uuid: &str) -> u8 {
((hash % 250) as u8) + 1 ((hash % 250) as u8) + 1
} }
pub(crate) fn stable_mavlink_identity(callsign: &str, entity_uuid: &str) -> String {
let mut identity = callsign.trim().to_string();
for suffix in [" [ON]", " [OFF]"] {
if identity.ends_with(suffix) {
identity.truncate(identity.len() - suffix.len());
break;
}
}
if identity.is_empty() {
entity_uuid.trim().to_string()
} else {
identity
}
}
fn uuid16(entity_uuid: &str) -> [u8; 16] { fn uuid16(entity_uuid: &str) -> [u8; 16] {
let hex = entity_uuid.replace('-', ""); let hex = entity_uuid.replace('-', "");
let mut bytes = [0u8; 16]; let mut bytes = [0u8; 16];

View File

@@ -6,6 +6,7 @@ mod identity;
mod packets; mod packets;
mod payload; mod payload;
mod send; mod send;
mod state;
pub use endpoint::{start_endpoint, stop_endpoint}; pub use endpoint::{start_endpoint, stop_endpoint};
#[allow(unused_imports)] #[allow(unused_imports)]

View File

@@ -1,16 +1,17 @@
use chrono::Utc; use chrono::Utc;
use super::constants::{ use super::constants::{
AUTOPILOT_COMPONENT_ID, CAMERA_CAP_FLAGS_HAS_VIDEO_STREAM, CAMERA_COMPONENT_ID, AUTOPILOT_COMPONENT_ID, CAMERA_CAP_FLAGS_CAPTURE_IMAGE, CAMERA_CAP_FLAGS_CAPTURE_VIDEO,
GIMBAL_COMPONENT_ID, MAV_AUTOPILOT_GENERIC, MAV_AUTOPILOT_INVALID, CAMERA_CAP_FLAGS_HAS_BASIC_ZOOM, CAMERA_CAP_FLAGS_HAS_MODES, CAMERA_CAP_FLAGS_HAS_VIDEO_STREAM,
MAV_LANDED_STATE_IN_AIR, MAV_LANDED_STATE_ON_GROUND, MAV_LANDED_STATE_UNDEFINED, GIMBAL_COMPONENT_ID, GIMBAL_MANAGER_CAP_FLAGS_BASIC_PITCH_YAW, MAV_AUTOPILOT_ARDUPILOTMEGA,
MAV_MODE_FLAG_CUSTOM_MODE_ENABLED, MAV_MODE_FLAG_SAFETY_ARMED, MAV_AUTOPILOT_INVALID, MAV_LANDED_STATE_IN_AIR, MAV_LANDED_STATE_ON_GROUND,
MAV_PROTOCOL_CAPABILITY_COMMAND_INT, MAV_PROTOCOL_CAPABILITY_COMPONENT_IMPLEMENTS_GIMBAL_MANAGER, MAV_LANDED_STATE_UNDEFINED, MAV_MODE_FLAG_CUSTOM_MODE_ENABLED, MAV_MODE_FLAG_SAFETY_ARMED,
MAV_PROTOCOL_CAPABILITY_FTP, MAV_PROTOCOL_CAPABILITY_MAVLINK2, MAV_PROTOCOL_CAPABILITY_COMMAND_INT,
MAV_PROTOCOL_CAPABILITY_MISSION_INT, MAV_PROTOCOL_CAPABILITY_SET_POSITION_TARGET_GLOBAL_INT, MAV_PROTOCOL_CAPABILITY_COMPONENT_IMPLEMENTS_GIMBAL_MANAGER, MAV_PROTOCOL_CAPABILITY_FTP,
MAV_STATE_ACTIVE, MAV_STATE_STANDBY, VIDEO_STREAM_ENCODING_H264, MAV_PROTOCOL_CAPABILITY_MAVLINK2, MAV_PROTOCOL_CAPABILITY_MISSION_INT,
VIDEO_STREAM_STATUS_FLAGS_RUNNING, VIDEO_STREAM_TYPE_MPEG_TS, VIDEO_STREAM_TYPE_RTPUDP, MAV_PROTOCOL_CAPABILITY_SET_POSITION_TARGET_GLOBAL_INT, MAV_STATE_ACTIVE, MAV_STATE_STANDBY,
VIDEO_STREAM_TYPE_RTSP, VIDEO_STREAM_TYPE_TCP_MPEG, VIDEO_STREAM_ENCODING_H264, VIDEO_STREAM_STATUS_FLAGS_RUNNING, VIDEO_STREAM_TYPE_MPEG_TS,
VIDEO_STREAM_TYPE_RTPUDP, VIDEO_STREAM_TYPE_RTSP, VIDEO_STREAM_TYPE_TCP_MPEG,
}; };
use super::crc::{build_v1_packet, build_v2_packet, calculate_crc_extra, FieldSpec}; use super::crc::{build_v1_packet, build_v2_packet, calculate_crc_extra, FieldSpec};
use super::identity::{fixed_string, normalize_heading_deg, uid2_from_uuid, uid64_from_uuid}; use super::identity::{fixed_string, normalize_heading_deg, uid2_from_uuid, uid64_from_uuid};
@@ -18,9 +19,10 @@ use super::payload::UasTelemetryPayload;
pub(crate) fn heartbeat_packet(payload: &UasTelemetryPayload) -> Vec<u8> { pub(crate) fn heartbeat_packet(payload: &UasTelemetryPayload) -> Vec<u8> {
let mut msg = Vec::with_capacity(9); let mut msg = Vec::with_capacity(9);
msg.extend_from_slice(&0u32.to_le_bytes()); let custom_mode = if payload.flying { 5u32 } else { 0u32 };
msg.extend_from_slice(&custom_mode.to_le_bytes());
msg.push(payload.vehicle_type); msg.push(payload.vehicle_type);
msg.push(MAV_AUTOPILOT_GENERIC); msg.push(MAV_AUTOPILOT_ARDUPILOTMEGA);
msg.push(if payload.flying { msg.push(if payload.flying {
MAV_MODE_FLAG_CUSTOM_MODE_ENABLED | MAV_MODE_FLAG_SAFETY_ARMED MAV_MODE_FLAG_CUSTOM_MODE_ENABLED | MAV_MODE_FLAG_SAFETY_ARMED
} else { } else {
@@ -35,6 +37,49 @@ pub(crate) fn heartbeat_packet(payload: &UasTelemetryPayload) -> Vec<u8> {
build_v1_packet(payload.system_id, payload.component_id, 0, &msg, 50) build_v1_packet(payload.system_id, payload.component_id, 0, &msg, 50)
} }
pub(crate) fn command_ack_packet(
system_id: u8,
component_id: u8,
command: u16,
result: u8,
) -> Vec<u8> {
let mut msg = Vec::with_capacity(3);
msg.extend_from_slice(&command.to_le_bytes());
msg.push(result);
build_v2_packet(system_id, component_id, 77, &msg, 143)
}
pub(crate) fn mission_request_int_packet(
system_id: u8,
component_id: u8,
target_system: u8,
target_component: u8,
seq: u16,
mission_type: u8,
) -> Vec<u8> {
let mut msg = Vec::with_capacity(5);
msg.extend_from_slice(&seq.to_le_bytes());
msg.push(target_system);
msg.push(target_component);
msg.push(mission_type);
build_v2_packet(system_id, component_id, 51, &msg, 196)
}
pub(crate) fn mission_ack_packet(
system_id: u8,
component_id: u8,
target_system: u8,
target_component: u8,
mission_type: u8,
) -> Vec<u8> {
let mut msg = Vec::with_capacity(4);
msg.push(target_system);
msg.push(target_component);
msg.push(0);
msg.push(mission_type);
build_v2_packet(system_id, component_id, 47, &msg, 153)
}
pub(crate) fn gps_raw_int_packet(payload: &UasTelemetryPayload) -> Vec<u8> { pub(crate) fn gps_raw_int_packet(payload: &UasTelemetryPayload) -> Vec<u8> {
let time_usec = (Utc::now().timestamp_millis().max(0) as u64) * 1_000; let time_usec = (Utc::now().timestamp_millis().max(0) as u64) * 1_000;
let fix_type = if payload.flying { 3u8 } else { 2u8 }; let fix_type = if payload.flying { 3u8 } else { 2u8 };
@@ -55,8 +100,10 @@ pub(crate) fn gps_raw_int_packet(payload: &UasTelemetryPayload) -> Vec<u8> {
pub(crate) fn global_position_int_packet(payload: &UasTelemetryPayload) -> Vec<u8> { pub(crate) fn global_position_int_packet(payload: &UasTelemetryPayload) -> Vec<u8> {
let time_boot_ms = Utc::now().timestamp_millis().max(0) as u32; let time_boot_ms = Utc::now().timestamp_millis().max(0) as u32;
let vx = (payload.groundspeed_mps * payload.heading_deg.to_radians().sin() * 100.0).round() as i16; let vx =
let vy = (payload.groundspeed_mps * payload.heading_deg.to_radians().cos() * 100.0).round() as i16; (payload.groundspeed_mps * payload.heading_deg.to_radians().sin() * 100.0).round() as i16;
let vy =
(payload.groundspeed_mps * payload.heading_deg.to_radians().cos() * 100.0).round() as i16;
let heading = (normalize_heading_deg(payload.heading_deg) * 100.0).round() as u16; let heading = (normalize_heading_deg(payload.heading_deg) * 100.0).round() as u16;
let mut msg = Vec::with_capacity(28); let mut msg = Vec::with_capacity(28);
@@ -103,21 +150,73 @@ pub(crate) fn vfr_hud_packet(payload: &UasTelemetryPayload) -> Vec<u8> {
build_v1_packet(payload.system_id, payload.component_id, 74, &msg, 20) build_v1_packet(payload.system_id, payload.component_id, 74, &msg, 20)
} }
pub(crate) fn system_status_packet(system_id: u8) -> Vec<u8> { pub(crate) fn system_status_packet(system_id: u8, battery_remaining_pct: i8) -> Vec<u8> {
let fields = [ let fields = [
FieldSpec { ty: "uint32_t", name: "onboard_control_sensors_present", array_len: 0 }, FieldSpec {
FieldSpec { ty: "uint32_t", name: "onboard_control_sensors_enabled", array_len: 0 }, ty: "uint32_t",
FieldSpec { ty: "uint32_t", name: "onboard_control_sensors_health", array_len: 0 }, name: "onboard_control_sensors_present",
FieldSpec { ty: "uint16_t", name: "load", array_len: 0 }, array_len: 0,
FieldSpec { ty: "uint16_t", name: "voltage_battery", array_len: 0 }, },
FieldSpec { ty: "int16_t", name: "current_battery", array_len: 0 }, FieldSpec {
FieldSpec { ty: "uint16_t", name: "drop_rate_comm", array_len: 0 }, ty: "uint32_t",
FieldSpec { ty: "uint16_t", name: "errors_comm", array_len: 0 }, name: "onboard_control_sensors_enabled",
FieldSpec { ty: "uint16_t", name: "errors_count1", array_len: 0 }, array_len: 0,
FieldSpec { ty: "uint16_t", name: "errors_count2", array_len: 0 }, },
FieldSpec { ty: "uint16_t", name: "errors_count3", array_len: 0 }, FieldSpec {
FieldSpec { ty: "uint16_t", name: "errors_count4", array_len: 0 }, ty: "uint32_t",
FieldSpec { ty: "int8_t", name: "battery_remaining", array_len: 0 }, name: "onboard_control_sensors_health",
array_len: 0,
},
FieldSpec {
ty: "uint16_t",
name: "load",
array_len: 0,
},
FieldSpec {
ty: "uint16_t",
name: "voltage_battery",
array_len: 0,
},
FieldSpec {
ty: "int16_t",
name: "current_battery",
array_len: 0,
},
FieldSpec {
ty: "uint16_t",
name: "drop_rate_comm",
array_len: 0,
},
FieldSpec {
ty: "uint16_t",
name: "errors_comm",
array_len: 0,
},
FieldSpec {
ty: "uint16_t",
name: "errors_count1",
array_len: 0,
},
FieldSpec {
ty: "uint16_t",
name: "errors_count2",
array_len: 0,
},
FieldSpec {
ty: "uint16_t",
name: "errors_count3",
array_len: 0,
},
FieldSpec {
ty: "uint16_t",
name: "errors_count4",
array_len: 0,
},
FieldSpec {
ty: "int8_t",
name: "battery_remaining",
array_len: 0,
},
]; ];
let crc_extra = calculate_crc_extra("SYS_STATUS", &fields); let crc_extra = calculate_crc_extra("SYS_STATUS", &fields);
@@ -135,24 +234,32 @@ pub(crate) fn system_status_packet(system_id: u8) -> Vec<u8> {
msg.extend_from_slice(&0u16.to_le_bytes()); msg.extend_from_slice(&0u16.to_le_bytes());
msg.extend_from_slice(&0u16.to_le_bytes()); msg.extend_from_slice(&0u16.to_le_bytes());
msg.extend_from_slice(&0u16.to_le_bytes()); msg.extend_from_slice(&0u16.to_le_bytes());
msg.push(100u8); msg.push(battery_remaining_pct.clamp(0, 100) as u8);
build_v2_packet(system_id, AUTOPILOT_COMPONENT_ID, 1, &msg, crc_extra) build_v2_packet(system_id, AUTOPILOT_COMPONENT_ID, 1, &msg, crc_extra)
} }
pub(crate) fn extended_sys_state_packet(system_id: u8, flying: bool) -> Vec<u8> { pub(crate) fn extended_sys_state_packet(system_id: u8, landed: bool) -> Vec<u8> {
let fields = [ let fields = [
FieldSpec { ty: "uint8_t", name: "vtol_state", array_len: 0 }, FieldSpec {
FieldSpec { ty: "uint8_t", name: "landed_state", array_len: 0 }, ty: "uint8_t",
name: "vtol_state",
array_len: 0,
},
FieldSpec {
ty: "uint8_t",
name: "landed_state",
array_len: 0,
},
]; ];
let crc_extra = calculate_crc_extra("EXTENDED_SYS_STATE", &fields); let crc_extra = calculate_crc_extra("EXTENDED_SYS_STATE", &fields);
let mut msg = Vec::with_capacity(2); let mut msg = Vec::with_capacity(2);
msg.push(MAV_LANDED_STATE_UNDEFINED); msg.push(MAV_LANDED_STATE_UNDEFINED);
msg.push(if flying { msg.push(if landed {
MAV_LANDED_STATE_IN_AIR
} else {
MAV_LANDED_STATE_ON_GROUND MAV_LANDED_STATE_ON_GROUND
} else {
MAV_LANDED_STATE_IN_AIR
}); });
build_v2_packet(system_id, AUTOPILOT_COMPONENT_ID, 245, &msg, crc_extra) build_v2_packet(system_id, AUTOPILOT_COMPONENT_ID, 245, &msg, crc_extra)
@@ -160,17 +267,61 @@ pub(crate) fn extended_sys_state_packet(system_id: u8, flying: bool) -> Vec<u8>
pub(crate) fn autopilot_version_packet(system_id: u8, entity_uuid: &str) -> Vec<u8> { pub(crate) fn autopilot_version_packet(system_id: u8, entity_uuid: &str) -> Vec<u8> {
let fields = [ let fields = [
FieldSpec { ty: "uint64_t", name: "capabilities", array_len: 0 }, FieldSpec {
FieldSpec { ty: "uint32_t", name: "flight_sw_version", array_len: 0 }, ty: "uint64_t",
FieldSpec { ty: "uint32_t", name: "middleware_sw_version", array_len: 0 }, name: "capabilities",
FieldSpec { ty: "uint32_t", name: "os_sw_version", array_len: 0 }, array_len: 0,
FieldSpec { ty: "uint32_t", name: "board_version", array_len: 0 }, },
FieldSpec { ty: "uint8_t", name: "flight_custom_version", array_len: 8 }, FieldSpec {
FieldSpec { ty: "uint8_t", name: "middleware_custom_version", array_len: 8 }, ty: "uint32_t",
FieldSpec { ty: "uint8_t", name: "os_custom_version", array_len: 8 }, name: "flight_sw_version",
FieldSpec { ty: "uint16_t", name: "vendor_id", array_len: 0 }, array_len: 0,
FieldSpec { ty: "uint16_t", name: "product_id", array_len: 0 }, },
FieldSpec { ty: "uint64_t", name: "uid", array_len: 0 }, FieldSpec {
ty: "uint32_t",
name: "middleware_sw_version",
array_len: 0,
},
FieldSpec {
ty: "uint32_t",
name: "os_sw_version",
array_len: 0,
},
FieldSpec {
ty: "uint32_t",
name: "board_version",
array_len: 0,
},
FieldSpec {
ty: "uint8_t",
name: "flight_custom_version",
array_len: 8,
},
FieldSpec {
ty: "uint8_t",
name: "middleware_custom_version",
array_len: 8,
},
FieldSpec {
ty: "uint8_t",
name: "os_custom_version",
array_len: 8,
},
FieldSpec {
ty: "uint16_t",
name: "vendor_id",
array_len: 0,
},
FieldSpec {
ty: "uint16_t",
name: "product_id",
array_len: 0,
},
FieldSpec {
ty: "uint64_t",
name: "uid",
array_len: 0,
},
]; ];
let crc_extra = calculate_crc_extra("AUTOPILOT_VERSION", &fields); let crc_extra = calculate_crc_extra("AUTOPILOT_VERSION", &fields);
@@ -200,7 +351,38 @@ pub(crate) fn autopilot_version_packet(system_id: u8, entity_uuid: &str) -> Vec<
build_v2_packet(system_id, AUTOPILOT_COMPONENT_ID, 148, &msg, crc_extra) build_v2_packet(system_id, AUTOPILOT_COMPONENT_ID, 148, &msg, crc_extra)
} }
pub(crate) fn component_heartbeat_packet(system_id: u8, component_id: u8, component_type: u8) -> Vec<u8> { pub(crate) fn home_position_packet(
system_id: u8,
lat_deg: f64,
lon_deg: f64,
alt_msl_m: f32,
heading_deg: f32,
) -> Vec<u8> {
let yaw = normalize_heading_deg(heading_deg).to_radians();
let q = [(yaw * 0.5).cos(), 0.0f32, 0.0f32, (yaw * 0.5).sin()];
let mut msg = Vec::with_capacity(52);
msg.extend_from_slice(&((lat_deg * 1e7).round() as i32).to_le_bytes());
msg.extend_from_slice(&((lon_deg * 1e7).round() as i32).to_le_bytes());
msg.extend_from_slice(&((alt_msl_m * 1000.0).round() as i32).to_le_bytes());
msg.extend_from_slice(&0f32.to_le_bytes());
msg.extend_from_slice(&0f32.to_le_bytes());
msg.extend_from_slice(&0f32.to_le_bytes());
for value in q {
msg.extend_from_slice(&value.to_le_bytes());
}
msg.extend_from_slice(&0f32.to_le_bytes());
msg.extend_from_slice(&0f32.to_le_bytes());
msg.extend_from_slice(&0f32.to_le_bytes());
build_v2_packet(system_id, AUTOPILOT_COMPONENT_ID, 242, &msg, 85)
}
pub(crate) fn component_heartbeat_packet(
system_id: u8,
component_id: u8,
component_type: u8,
) -> Vec<u8> {
let mut msg = Vec::with_capacity(9); let mut msg = Vec::with_capacity(9);
msg.extend_from_slice(&0u32.to_le_bytes()); msg.extend_from_slice(&0u32.to_le_bytes());
msg.push(component_type); msg.push(component_type);
@@ -211,10 +393,20 @@ pub(crate) fn component_heartbeat_packet(system_id: u8, component_id: u8, compon
build_v2_packet(system_id, component_id, 0, &msg, 50) build_v2_packet(system_id, component_id, 0, &msg, 50)
} }
pub(crate) fn camera_information_packet(system_id: u8, callsign: &str) -> Vec<u8> { pub(crate) fn camera_information_packet_for_component(
system_id: u8,
component_id: u8,
callsign: &str,
gimbal_device_id: u8,
) -> Vec<u8> {
let vendor = fixed_string::<32>("ArmaTAK"); let vendor = fixed_string::<32>("ArmaTAK");
let model = fixed_string::<32>(callsign); let model = fixed_string::<32>(callsign);
let cam_definition_uri = fixed_string::<140>(""); let cam_definition_uri = fixed_string::<140>("");
let flags = CAMERA_CAP_FLAGS_CAPTURE_VIDEO
| CAMERA_CAP_FLAGS_CAPTURE_IMAGE
| CAMERA_CAP_FLAGS_HAS_MODES
| CAMERA_CAP_FLAGS_HAS_BASIC_ZOOM
| CAMERA_CAP_FLAGS_HAS_VIDEO_STREAM;
let mut msg = Vec::with_capacity(235); let mut msg = Vec::with_capacity(235);
msg.extend_from_slice(&(Utc::now().timestamp_millis().max(0) as u32).to_le_bytes()); msg.extend_from_slice(&(Utc::now().timestamp_millis().max(0) as u32).to_le_bytes());
@@ -222,7 +414,7 @@ pub(crate) fn camera_information_packet(system_id: u8, callsign: &str) -> Vec<u8
msg.extend_from_slice(&2.8f32.to_le_bytes()); msg.extend_from_slice(&2.8f32.to_le_bytes());
msg.extend_from_slice(&6.4f32.to_le_bytes()); msg.extend_from_slice(&6.4f32.to_le_bytes());
msg.extend_from_slice(&4.8f32.to_le_bytes()); msg.extend_from_slice(&4.8f32.to_le_bytes());
msg.extend_from_slice(&CAMERA_CAP_FLAGS_HAS_VIDEO_STREAM.to_le_bytes()); msg.extend_from_slice(&flags.to_le_bytes());
msg.extend_from_slice(&1280u16.to_le_bytes()); msg.extend_from_slice(&1280u16.to_le_bytes());
msg.extend_from_slice(&720u16.to_le_bytes()); msg.extend_from_slice(&720u16.to_le_bytes());
msg.extend_from_slice(&0u16.to_le_bytes()); msg.extend_from_slice(&0u16.to_le_bytes());
@@ -230,17 +422,21 @@ pub(crate) fn camera_information_packet(system_id: u8, callsign: &str) -> Vec<u8
msg.extend_from_slice(&model); msg.extend_from_slice(&model);
msg.push(0); msg.push(0);
msg.extend_from_slice(&cam_definition_uri); msg.extend_from_slice(&cam_definition_uri);
msg.push(GIMBAL_COMPONENT_ID); msg.push(gimbal_device_id);
msg.push(0); msg.push(0);
build_v2_packet(system_id, CAMERA_COMPONENT_ID, 259, &msg, 92) build_v2_packet(system_id, component_id, 259, &msg, 92)
} }
pub(crate) fn video_stream_information_packet( pub(crate) fn video_stream_information_packet_for_component(
system_id: u8, system_id: u8,
component_id: u8,
callsign: &str, callsign: &str,
video_uri: &str, video_uri: &str,
hfov_deg: f32, hfov_deg: f32,
stream_id: u8,
stream_count: u8,
thermal: bool,
) -> Vec<u8> { ) -> Vec<u8> {
let stream_type = if video_uri.starts_with("rtsp://") { let stream_type = if video_uri.starts_with("rtsp://") {
VIDEO_STREAM_TYPE_RTSP VIDEO_STREAM_TYPE_RTSP
@@ -254,42 +450,195 @@ pub(crate) fn video_stream_information_packet(
VIDEO_STREAM_TYPE_RTSP VIDEO_STREAM_TYPE_RTSP
}; };
let name = fixed_string::<32>(&format!("{} Video", callsign)); let name = fixed_string::<32>(callsign);
let uri = fixed_string::<160>(video_uri); let uri = fixed_string::<160>(video_uri);
let flags = VIDEO_STREAM_STATUS_FLAGS_RUNNING | if thermal { 2 } else { 0 };
let mut msg = Vec::with_capacity(208); let mut msg = Vec::with_capacity(208);
msg.extend_from_slice(&30f32.to_le_bytes()); msg.extend_from_slice(&30f32.to_le_bytes());
msg.extend_from_slice(&4_000_000u32.to_le_bytes()); msg.extend_from_slice(&4_000_000u32.to_le_bytes());
msg.extend_from_slice(&VIDEO_STREAM_STATUS_FLAGS_RUNNING.to_le_bytes()); msg.extend_from_slice(&flags.to_le_bytes());
msg.extend_from_slice(&1280u16.to_le_bytes()); msg.extend_from_slice(&1280u16.to_le_bytes());
msg.extend_from_slice(&720u16.to_le_bytes()); msg.extend_from_slice(&720u16.to_le_bytes());
msg.extend_from_slice(&0u16.to_le_bytes()); msg.extend_from_slice(&0u16.to_le_bytes());
msg.extend_from_slice(&(hfov_deg.clamp(1.0, 360.0).round() as u16).to_le_bytes()); msg.extend_from_slice(&(hfov_deg.clamp(1.0, 360.0).round() as u16).to_le_bytes());
msg.push(1); msg.push(stream_id);
msg.push(1); msg.push(stream_count);
msg.push(stream_type); msg.push(stream_type);
msg.extend_from_slice(&name); msg.extend_from_slice(&name);
msg.extend_from_slice(&uri); msg.extend_from_slice(&uri);
msg.push(VIDEO_STREAM_ENCODING_H264); msg.push(VIDEO_STREAM_ENCODING_H264);
msg.push(0); msg.push(0);
build_v2_packet(system_id, CAMERA_COMPONENT_ID, 269, &msg, 109) build_v2_packet(system_id, component_id, 269, &msg, 109)
} }
pub(crate) fn video_stream_status_packet( pub(crate) fn video_stream_status_packet_for_component(
system_id: u8, system_id: u8,
component_id: u8,
hfov_deg: f32, hfov_deg: f32,
stream_id: u8,
thermal: bool,
) -> Vec<u8> { ) -> Vec<u8> {
let flags = VIDEO_STREAM_STATUS_FLAGS_RUNNING | if thermal { 2 } else { 0 };
let mut msg = Vec::with_capacity(19); let mut msg = Vec::with_capacity(19);
msg.extend_from_slice(&30f32.to_le_bytes()); msg.extend_from_slice(&30f32.to_le_bytes());
msg.extend_from_slice(&4_000_000u32.to_le_bytes()); msg.extend_from_slice(&4_000_000u32.to_le_bytes());
msg.extend_from_slice(&VIDEO_STREAM_STATUS_FLAGS_RUNNING.to_le_bytes()); msg.extend_from_slice(&flags.to_le_bytes());
msg.extend_from_slice(&1280u16.to_le_bytes()); msg.extend_from_slice(&1280u16.to_le_bytes());
msg.extend_from_slice(&720u16.to_le_bytes()); msg.extend_from_slice(&720u16.to_le_bytes());
msg.extend_from_slice(&0u16.to_le_bytes()); msg.extend_from_slice(&0u16.to_le_bytes());
msg.extend_from_slice(&(hfov_deg.clamp(1.0, 360.0).round() as u16).to_le_bytes()); msg.extend_from_slice(&(hfov_deg.clamp(1.0, 360.0).round() as u16).to_le_bytes());
msg.push(1); msg.push(stream_id);
msg.push(0); msg.push(0);
build_v2_packet(system_id, CAMERA_COMPONENT_ID, 270, &msg, 59) build_v2_packet(system_id, component_id, 270, &msg, 59)
}
pub(crate) fn mount_orientation_packet_for_component(
system_id: u8,
component_id: u8,
pitch_deg: f32,
yaw_deg: f32,
) -> Vec<u8> {
let mut msg = Vec::with_capacity(16);
msg.extend_from_slice(&(Utc::now().timestamp_millis().max(0) as u32).to_le_bytes());
msg.extend_from_slice(&pitch_deg.to_le_bytes());
msg.extend_from_slice(&0f32.to_le_bytes());
msg.extend_from_slice(&normalize_heading_deg(yaw_deg).to_le_bytes());
build_v2_packet(system_id, component_id, 265, &msg, 26)
}
pub(crate) fn camera_fov_status_packet_for_component(
system_id: u8,
component_id: u8,
lat_camera_deg: f64,
lon_camera_deg: f64,
alt_camera_msl_m: f32,
lat_image_deg: f64,
lon_image_deg: f64,
alt_image_msl_m: f32,
roll_deg: f32,
pitch_deg: f32,
yaw_deg: f32,
hfov_deg: f32,
vfov_deg: f32,
) -> Vec<u8> {
let q = attitude_quaternion(roll_deg, pitch_deg, yaw_deg);
let mut msg = Vec::with_capacity(53);
msg.extend_from_slice(&(Utc::now().timestamp_millis().max(0) as u32).to_le_bytes());
msg.extend_from_slice(&((lat_camera_deg * 1e7).round() as i32).to_le_bytes());
msg.extend_from_slice(&((lon_camera_deg * 1e7).round() as i32).to_le_bytes());
msg.extend_from_slice(&((alt_camera_msl_m * 1000.0).round() as i32).to_le_bytes());
msg.extend_from_slice(&((lat_image_deg * 1e7).round() as i32).to_le_bytes());
msg.extend_from_slice(&((lon_image_deg * 1e7).round() as i32).to_le_bytes());
msg.extend_from_slice(&((alt_image_msl_m * 1000.0).round() as i32).to_le_bytes());
for value in q {
msg.extend_from_slice(&value.to_le_bytes());
}
msg.extend_from_slice(&hfov_deg.to_le_bytes());
msg.extend_from_slice(&vfov_deg.to_le_bytes());
msg.push(0);
build_v2_packet(system_id, component_id, 271, &msg, 22)
}
pub(crate) fn mount_status_packet(
system_id: u8,
pitch_deg: f32,
roll_deg: f32,
relative_yaw_deg: f32,
) -> Vec<u8> {
let mut msg = Vec::with_capacity(15);
msg.extend_from_slice(&((pitch_deg * 100.0).round() as i32).to_le_bytes());
msg.extend_from_slice(&((roll_deg * 100.0).round() as i32).to_le_bytes());
msg.extend_from_slice(&((relative_yaw_deg * 100.0).round() as i32).to_le_bytes());
msg.push(system_id);
msg.push(GIMBAL_COMPONENT_ID);
msg.push(2);
build_v2_packet(system_id, AUTOPILOT_COMPONENT_ID, 158, &msg, 134)
}
pub(crate) fn gimbal_manager_information_packet(system_id: u8) -> Vec<u8> {
let fields = [
FieldSpec {
ty: "uint32_t",
name: "time_boot_ms",
array_len: 0,
},
FieldSpec {
ty: "uint32_t",
name: "cap_flags",
array_len: 0,
},
FieldSpec {
ty: "float",
name: "roll_min",
array_len: 0,
},
FieldSpec {
ty: "float",
name: "roll_max",
array_len: 0,
},
FieldSpec {
ty: "float",
name: "pitch_min",
array_len: 0,
},
FieldSpec {
ty: "float",
name: "pitch_max",
array_len: 0,
},
FieldSpec {
ty: "float",
name: "yaw_min",
array_len: 0,
},
FieldSpec {
ty: "float",
name: "yaw_max",
array_len: 0,
},
FieldSpec {
ty: "uint8_t",
name: "gimbal_device_id",
array_len: 0,
},
];
let crc_extra = calculate_crc_extra("GIMBAL_MANAGER_INFORMATION", &fields);
let mut msg = Vec::with_capacity(33);
msg.extend_from_slice(&(Utc::now().timestamp_millis().max(0) as u32).to_le_bytes());
msg.extend_from_slice(&GIMBAL_MANAGER_CAP_FLAGS_BASIC_PITCH_YAW.to_le_bytes());
msg.extend_from_slice(&0f32.to_le_bytes());
msg.extend_from_slice(&0f32.to_le_bytes());
msg.extend_from_slice(&(-90f32).to_radians().to_le_bytes());
msg.extend_from_slice(&30f32.to_radians().to_le_bytes());
msg.extend_from_slice(&(-180f32).to_radians().to_le_bytes());
msg.extend_from_slice(&180f32.to_radians().to_le_bytes());
msg.push(GIMBAL_COMPONENT_ID);
build_v2_packet(system_id, GIMBAL_COMPONENT_ID, 280, &msg, crc_extra)
}
fn attitude_quaternion(roll_deg: f32, pitch_deg: f32, yaw_deg: f32) -> [f32; 4] {
let (roll, pitch, yaw) = (
roll_deg.to_radians(),
pitch_deg.to_radians(),
normalize_heading_deg(yaw_deg).to_radians(),
);
let (sr, cr) = (roll * 0.5).sin_cos();
let (sp, cp) = (pitch * 0.5).sin_cos();
let (sy, cy) = (yaw * 0.5).sin_cos();
[
cr * cp * cy + sr * sp * sy,
sr * cp * cy - cr * sp * sy,
cr * sp * cy + sr * cp * sy,
cr * cp * sy - sr * sp * cy,
]
} }

View File

@@ -33,6 +33,7 @@ pub struct UasSystemPayload {
pub pitch_deg: f32, pub pitch_deg: f32,
pub yaw_deg: f32, pub yaw_deg: f32,
pub flying: bool, pub flying: bool,
pub landed: bool,
pub gimbal_roll_deg: f32, pub gimbal_roll_deg: f32,
pub gimbal_pitch_deg: f32, pub gimbal_pitch_deg: f32,
pub gimbal_yaw_deg: f32, pub gimbal_yaw_deg: f32,
@@ -42,6 +43,8 @@ pub struct UasSystemPayload {
pub image_lat_deg: f64, pub image_lat_deg: f64,
pub image_lon_deg: f64, pub image_lon_deg: f64,
pub image_alt_msl_m: f32, pub image_alt_msl_m: f32,
pub has_turret_camera: bool,
pub battery_remaining_pct: i8,
} }
impl FromArma for UasTelemetryPayload { impl FromArma for UasTelemetryPayload {
@@ -114,6 +117,7 @@ impl FromArma for UasSystemPayload {
pitch_deg, pitch_deg,
yaw_deg, yaw_deg,
flying, flying,
landed,
gimbal_roll_deg, gimbal_roll_deg,
gimbal_pitch_deg, gimbal_pitch_deg,
gimbal_yaw_deg, gimbal_yaw_deg,
@@ -123,6 +127,8 @@ impl FromArma for UasSystemPayload {
image_lat_deg, image_lat_deg,
image_lon_deg, image_lon_deg,
image_alt_msl_m, image_alt_msl_m,
has_turret_camera,
battery_remaining_pct,
) = <( ) = <(
String, String,
String, String,
@@ -138,6 +144,7 @@ impl FromArma for UasSystemPayload {
f32, f32,
f32, f32,
i32, i32,
i32,
f32, f32,
f32, f32,
f32, f32,
@@ -147,6 +154,8 @@ impl FromArma for UasSystemPayload {
f64, f64,
f64, f64,
f32, f32,
i32,
i32,
)>::from_arma(data)?; )>::from_arma(data)?;
Ok(Self { Ok(Self {
@@ -164,6 +173,7 @@ impl FromArma for UasSystemPayload {
pitch_deg, pitch_deg,
yaw_deg, yaw_deg,
flying: flying != 0, flying: flying != 0,
landed: landed != 0,
gimbal_roll_deg, gimbal_roll_deg,
gimbal_pitch_deg, gimbal_pitch_deg,
gimbal_yaw_deg, gimbal_yaw_deg,
@@ -173,6 +183,8 @@ impl FromArma for UasSystemPayload {
image_lat_deg, image_lat_deg,
image_lon_deg, image_lon_deg,
image_alt_msl_m, image_alt_msl_m,
has_turret_camera: has_turret_camera != 0,
battery_remaining_pct: battery_remaining_pct.clamp(0, 100) as i8,
}) })
} }
} }

View File

@@ -2,16 +2,25 @@ use arma_rs::Context;
use log::info; use log::info;
use std::net::UdpSocket; use std::net::UdpSocket;
use super::constants::{AUTOPILOT_COMPONENT_ID, CAMERA_COMPONENT_ID, GIMBAL_COMPONENT_ID, MAV_TYPE_CAMERA, MAV_TYPE_GIMBAL}; use super::constants::{
AUTOPILOT_COMPONENT_ID, CAMERA_COMPONENT_ID, GIMBAL_COMPONENT_ID, MAV_TYPE_CAMERA,
MAV_TYPE_GIMBAL, TURRET_CAMERA_COMPONENT_ID,
};
use super::endpoint::socket_for_send; use super::endpoint::socket_for_send;
use super::identity::{map_vehicle_type, should_send_video_stream_information, stable_system_id}; use super::identity::{
map_vehicle_type, should_send_video_stream_information, stable_mavlink_identity,
stable_system_id,
};
use super::packets::{ use super::packets::{
attitude_packet, autopilot_version_packet, camera_information_packet, attitude_packet, autopilot_version_packet, camera_fov_status_packet_for_component,
component_heartbeat_packet, extended_sys_state_packet, global_position_int_packet, camera_information_packet_for_component, component_heartbeat_packet, extended_sys_state_packet,
gps_raw_int_packet, heartbeat_packet, system_status_packet, vfr_hud_packet, gimbal_manager_information_packet, global_position_int_packet, gps_raw_int_packet,
video_stream_information_packet, video_stream_status_packet, heartbeat_packet, home_position_packet, mount_orientation_packet_for_component,
mount_status_packet, system_status_packet, vfr_hud_packet,
video_stream_information_packet_for_component, video_stream_status_packet_for_component,
}; };
use super::payload::{UasSystemPayload, UasTelemetryPayload}; use super::payload::{UasSystemPayload, UasTelemetryPayload};
use super::state::{latest_system, record_system};
fn sending_socket(ctx: &Context, error_prefix: &str) -> Result<UdpSocket, &'static str> { fn sending_socket(ctx: &Context, error_prefix: &str) -> Result<UdpSocket, &'static str> {
if let Some(socket) = socket_for_send() { if let Some(socket) = socket_for_send() {
@@ -21,7 +30,11 @@ fn sending_socket(ctx: &Context, error_prefix: &str) -> Result<UdpSocket, &'stat
match UdpSocket::bind("0.0.0.0:0") { match UdpSocket::bind("0.0.0.0:0") {
Ok(socket) => Ok(socket), Ok(socket) => Ok(socket),
Err(error) => { Err(error) => {
let _ = ctx.callback_data("MAVLINK MOCK ERROR", "Failed to bind UDP socket", error.to_string()); let _ = ctx.callback_data(
"MAVLINK MOCK ERROR",
"Failed to bind UDP socket",
error.to_string(),
);
info!("{} failed to bind UDP socket: {}", error_prefix, error); info!("{} failed to bind UDP socket: {}", error_prefix, error);
Err("Failed to bind MAVLink mock socket") Err("Failed to bind MAVLink mock socket")
} }
@@ -58,24 +71,54 @@ pub fn send_uas_telemetry(ctx: Context, payload: UasTelemetryPayload) -> &'stati
for (index, packet) in packets.iter().enumerate() { for (index, packet) in packets.iter().enumerate() {
if let Err(error) = socket.send_to(packet, &payload.address) { if let Err(error) = socket.send_to(packet, &payload.address) {
let _ = ctx.callback_data("MAVLINK MOCK ERROR", "Failed to send MAVLink packet", error.to_string()); let _ = ctx.callback_data(
info!("MAVLink mock failed sending packet {} to {}: {}", index, payload.address, error); "MAVLINK MOCK ERROR",
"Failed to send MAVLink packet",
error.to_string(),
);
info!(
"MAVLink mock failed sending packet {} to {}: {}",
index, payload.address, error
);
return "Failed to send MAVLink mock telemetry"; return "Failed to send MAVLink mock telemetry";
} }
} }
info!("MAVLink mock sent {} packets to {}", packets.len(), payload.address); info!(
"MAVLink mock sent {} packets to {}",
packets.len(),
payload.address
);
"Sent MAVLink mock telemetry" "Sent MAVLink mock telemetry"
} }
pub fn send_uas_system(ctx: Context, payload: UasSystemPayload) -> &'static str { pub fn send_uas_system(ctx: Context, payload: UasSystemPayload) -> &'static str {
let system_id = stable_system_id(&payload.entity_uuid); let mavlink_identity = stable_mavlink_identity(&payload.callsign, &payload.entity_uuid);
let system_id = stable_system_id(&mavlink_identity);
let vehicle_type = map_vehicle_type(payload.vehicle_type); let vehicle_type = map_vehicle_type(payload.vehicle_type);
record_system(system_id, &mavlink_identity, &payload);
let active_camera_component = latest_system(system_id)
.map(|system| system.active_camera_component)
.unwrap_or(CAMERA_COMPONENT_ID);
let (home_lat_deg, home_lon_deg, home_alt_msl_m) = latest_system(system_id)
.map(|system| {
(
system.home_lat_deg,
system.home_lon_deg,
system.home_alt_msl_m,
)
})
.unwrap_or((
payload.lat_deg,
payload.lon_deg,
payload.alt_msl_m - payload.rel_alt_m,
));
info!( info!(
"MAVLink system send requested to {} entity_uuid={} sysid={} callsign={} lat={} lon={} alt_msl={} rel_alt={} heading={} gimbal_pitch={} gimbal_yaw={} video_uri={}", "MAVLink system send requested to {} entity_uuid={} mavlink_identity={} sysid={} callsign={} lat={} lon={} alt_msl={} rel_alt={} heading={} gimbal_pitch={} gimbal_yaw={} video_uri={}",
payload.address, payload.address,
payload.entity_uuid, payload.entity_uuid,
mavlink_identity,
system_id, system_id,
payload.callsign, payload.callsign,
payload.lat_deg, payload.lat_deg,
@@ -92,6 +135,46 @@ pub fn send_uas_system(ctx: Context, payload: UasSystemPayload) -> &'static str
Ok(socket) => socket, Ok(socket) => socket,
Err(message) => return message, Err(message) => return message,
}; };
let (fpv_image_lat, fpv_image_lon, fpv_image_alt) = fpv_image_point(
payload.lat_deg,
payload.lon_deg,
payload.alt_msl_m,
payload.rel_alt_m,
payload.pitch_deg,
payload.yaw_deg,
);
let active_is_turret =
payload.has_turret_camera && active_camera_component == TURRET_CAMERA_COMPONENT_ID;
info!(
"MAVLink active camera sysid={} active_component={} has_turret={} active_is_turret={}",
system_id, active_camera_component, payload.has_turret_camera, active_is_turret
);
let (
primary_pitch,
primary_roll,
primary_yaw,
primary_image_lat,
primary_image_lon,
primary_image_alt,
) = if active_is_turret {
(
payload.gimbal_pitch_deg,
payload.gimbal_roll_deg,
payload.gimbal_yaw_deg,
payload.image_lat_deg,
payload.image_lon_deg,
payload.image_alt_msl_m,
)
} else {
(
payload.pitch_deg,
payload.roll_deg,
payload.yaw_deg,
fpv_image_lat,
fpv_image_lon,
fpv_image_alt,
)
};
let autopilot_payload = UasTelemetryPayload { let autopilot_payload = UasTelemetryPayload {
address: payload.address.clone(), address: payload.address.clone(),
@@ -116,27 +199,141 @@ pub fn send_uas_system(ctx: Context, payload: UasSystemPayload) -> &'static str
global_position_int_packet(&autopilot_payload), global_position_int_packet(&autopilot_payload),
attitude_packet(&autopilot_payload), attitude_packet(&autopilot_payload),
vfr_hud_packet(&autopilot_payload), vfr_hud_packet(&autopilot_payload),
system_status_packet(system_id), system_status_packet(system_id, payload.battery_remaining_pct),
extended_sys_state_packet(system_id, payload.flying), extended_sys_state_packet(system_id, payload.landed),
autopilot_version_packet(system_id, &payload.entity_uuid), autopilot_version_packet(system_id, &mavlink_identity),
home_position_packet(
system_id,
home_lat_deg,
home_lon_deg,
home_alt_msl_m,
payload.heading_deg,
),
component_heartbeat_packet(system_id, CAMERA_COMPONENT_ID, MAV_TYPE_CAMERA), component_heartbeat_packet(system_id, CAMERA_COMPONENT_ID, MAV_TYPE_CAMERA),
component_heartbeat_packet(system_id, GIMBAL_COMPONENT_ID, MAV_TYPE_GIMBAL), component_heartbeat_packet(system_id, GIMBAL_COMPONENT_ID, MAV_TYPE_GIMBAL),
camera_information_packet(system_id, &payload.callsign), camera_information_packet_for_component(
system_id,
CAMERA_COMPONENT_ID,
&format!("{} FPV", payload.callsign),
0,
),
mount_orientation_packet_for_component(
system_id,
CAMERA_COMPONENT_ID,
primary_pitch,
primary_yaw,
),
camera_fov_status_packet_for_component(
system_id,
CAMERA_COMPONENT_ID,
payload.lat_deg,
payload.lon_deg,
payload.alt_msl_m,
primary_image_lat,
primary_image_lon,
primary_image_alt,
primary_roll,
primary_pitch,
primary_yaw,
payload.hfov_deg,
payload.vfov_deg,
),
gimbal_manager_information_packet(system_id),
]; ];
if payload.has_turret_camera {
packets.push(component_heartbeat_packet(
system_id,
TURRET_CAMERA_COMPONENT_ID,
MAV_TYPE_CAMERA,
));
packets.push(camera_information_packet_for_component(
system_id,
TURRET_CAMERA_COMPONENT_ID,
&format!("{} Turret", payload.callsign),
GIMBAL_COMPONENT_ID,
));
packets.push(mount_orientation_packet_for_component(
system_id,
TURRET_CAMERA_COMPONENT_ID,
payload.gimbal_pitch_deg,
payload.gimbal_yaw_deg,
));
packets.push(camera_fov_status_packet_for_component(
system_id,
TURRET_CAMERA_COMPONENT_ID,
payload.lat_deg,
payload.lon_deg,
payload.alt_msl_m,
payload.image_lat_deg,
payload.image_lon_deg,
payload.image_alt_msl_m,
payload.gimbal_roll_deg,
payload.gimbal_pitch_deg,
payload.gimbal_yaw_deg,
payload.hfov_deg,
payload.vfov_deg,
));
}
let (active_pitch, active_roll, active_relative_yaw) = if active_is_turret {
(
payload.gimbal_pitch_deg,
payload.gimbal_roll_deg,
normalize_signed_deg(payload.gimbal_yaw_deg - payload.yaw_deg),
)
} else {
(payload.pitch_deg, payload.roll_deg, 0.0)
};
packets.push(mount_status_packet(
system_id,
active_pitch,
active_roll,
active_relative_yaw,
));
if should_send_video_stream_information(&payload.video_uri) { if should_send_video_stream_information(&payload.video_uri) {
info!( info!(
"Sending VIDEO_STREAM_INFORMATION for sysid={} uri={}", "Sending VIDEO_STREAM_INFORMATION for sysid={} uri={}",
system_id, system_id, payload.video_uri
payload.video_uri
); );
packets.push(video_stream_information_packet( packets.push(video_stream_information_packet_for_component(
system_id, system_id,
&payload.callsign, CAMERA_COMPONENT_ID,
&format!("{} FPV", payload.callsign),
&payload.video_uri, &payload.video_uri,
payload.hfov_deg, payload.hfov_deg,
1,
1,
false,
)); ));
packets.push(video_stream_status_packet(system_id, payload.hfov_deg)); packets.push(video_stream_status_packet_for_component(
system_id,
CAMERA_COMPONENT_ID,
payload.hfov_deg,
1,
false,
));
if payload.has_turret_camera {
packets.push(video_stream_information_packet_for_component(
system_id,
TURRET_CAMERA_COMPONENT_ID,
&format!("{} Turret", payload.callsign),
&payload.video_uri,
payload.hfov_deg,
1,
1,
false,
));
packets.push(video_stream_status_packet_for_component(
system_id,
TURRET_CAMERA_COMPONENT_ID,
payload.hfov_deg,
1,
false,
));
}
} else if !payload.video_uri.trim().is_empty() { } else if !payload.video_uri.trim().is_empty() {
info!( info!(
"Skipping VIDEO_STREAM_INFORMATION for sysid={} because URI is not a supported stream URI: {}", "Skipping VIDEO_STREAM_INFORMATION for sysid={} because URI is not a supported stream URI: {}",
@@ -146,8 +343,15 @@ pub fn send_uas_system(ctx: Context, payload: UasSystemPayload) -> &'static str
for (index, packet) in packets.iter().enumerate() { for (index, packet) in packets.iter().enumerate() {
if let Err(error) = socket.send_to(packet, &payload.address) { if let Err(error) = socket.send_to(packet, &payload.address) {
let _ = ctx.callback_data("MAVLINK MOCK ERROR", "Failed to send MAVLink packet", error.to_string()); let _ = ctx.callback_data(
info!("MAVLink system failed sending packet {} to {}: {}", index, payload.address, error); "MAVLINK MOCK ERROR",
"Failed to send MAVLink packet",
error.to_string(),
);
info!(
"MAVLink system failed sending packet {} to {}: {}",
index, payload.address, error
);
return "Failed to send MAVLink system telemetry"; return "Failed to send MAVLink system telemetry";
} }
} }
@@ -162,3 +366,38 @@ pub fn send_uas_system(ctx: Context, payload: UasSystemPayload) -> &'static str
); );
"Sent MAVLink system telemetry" "Sent MAVLink system telemetry"
} }
fn normalize_signed_deg(value: f32) -> f32 {
let normalized = ((value % 360.0) + 360.0) % 360.0;
if normalized > 180.0 {
normalized - 360.0
} else {
normalized
}
}
fn fpv_image_point(
lat_deg: f64,
lon_deg: f64,
alt_msl_m: f32,
rel_alt_m: f32,
pitch_deg: f32,
yaw_deg: f32,
) -> (f64, f64, f32) {
let pitch_rad = pitch_deg.to_radians();
let vertical = (-pitch_rad.sin()).max(0.01);
let slant_m = (rel_alt_m.max(1.0) / vertical).clamp(1.0, 15_000.0);
let ground_m = slant_m * pitch_rad.cos().abs();
let yaw_rad = yaw_deg.to_radians();
let north_m = ground_m * yaw_rad.cos();
let east_m = ground_m * yaw_rad.sin();
let lat_rad = lat_deg.to_radians();
let meters_per_degree_lat = 111_320.0;
let meters_per_degree_lon = (111_320.0 * lat_rad.cos().abs()).max(1.0);
(
lat_deg + north_m as f64 / meters_per_degree_lat,
lon_deg + east_m as f64 / meters_per_degree_lon,
alt_msl_m - rel_alt_m,
)
}

112
src/uas/state.rs Normal file
View File

@@ -0,0 +1,112 @@
use std::collections::HashMap;
use std::sync::Mutex;
use lazy_static::lazy_static;
use super::payload::UasSystemPayload;
#[derive(Clone)]
pub(crate) struct LatestUasSystem {
pub mavlink_identity: String,
pub callsign: String,
pub lat_deg: f64,
pub lon_deg: f64,
pub alt_msl_m: f32,
pub rel_alt_m: f32,
pub heading_deg: f32,
pub fpv_pitch_deg: f32,
pub fpv_yaw_deg: f32,
pub gimbal_pitch_deg: f32,
pub gimbal_yaw_deg: f32,
pub video_uri: String,
pub hfov_deg: f32,
pub vfov_deg: f32,
pub image_lat_deg: f64,
pub image_lon_deg: f64,
pub image_alt_msl_m: f32,
pub has_turret_camera: bool,
pub active_camera_component: u8,
pub home_lat_deg: f64,
pub home_lon_deg: f64,
pub home_alt_msl_m: f32,
}
lazy_static! {
static ref LATEST_UAS_SYSTEMS: Mutex<HashMap<u8, LatestUasSystem>> = Mutex::new(HashMap::new());
}
pub(crate) fn record_system(system_id: u8, mavlink_identity: &str, payload: &UasSystemPayload) {
if let Ok(mut systems) = LATEST_UAS_SYSTEMS.lock() {
let active_camera_component = systems
.get(&system_id)
.map(|system| system.active_camera_component)
.unwrap_or(super::constants::CAMERA_COMPONENT_ID);
let home = systems
.get(&system_id)
.map(|system| {
(
system.home_lat_deg,
system.home_lon_deg,
system.home_alt_msl_m,
)
})
.unwrap_or((
payload.lat_deg,
payload.lon_deg,
payload.alt_msl_m - payload.rel_alt_m,
));
systems.insert(
system_id,
LatestUasSystem {
mavlink_identity: mavlink_identity.to_string(),
callsign: payload.callsign.clone(),
lat_deg: payload.lat_deg,
lon_deg: payload.lon_deg,
alt_msl_m: payload.alt_msl_m,
rel_alt_m: payload.rel_alt_m,
heading_deg: payload.heading_deg,
fpv_pitch_deg: payload.pitch_deg,
fpv_yaw_deg: payload.yaw_deg,
gimbal_pitch_deg: payload.gimbal_pitch_deg,
gimbal_yaw_deg: payload.gimbal_yaw_deg,
video_uri: payload.video_uri.clone(),
hfov_deg: payload.hfov_deg,
vfov_deg: payload.vfov_deg,
image_lat_deg: payload.image_lat_deg,
image_lon_deg: payload.image_lon_deg,
image_alt_msl_m: payload.image_alt_msl_m,
has_turret_camera: payload.has_turret_camera,
active_camera_component,
home_lat_deg: home.0,
home_lon_deg: home.1,
home_alt_msl_m: home.2,
},
);
}
}
pub(crate) fn set_home(system_id: u8, lat_deg: f64, lon_deg: f64, alt_msl_m: f32) {
if let Ok(mut systems) = LATEST_UAS_SYSTEMS.lock() {
if let Some(system) = systems.get_mut(&system_id) {
system.home_lat_deg = lat_deg;
system.home_lon_deg = lon_deg;
system.home_alt_msl_m = alt_msl_m;
}
}
}
pub(crate) fn latest_system(system_id: u8) -> Option<LatestUasSystem> {
LATEST_UAS_SYSTEMS
.lock()
.ok()
.and_then(|systems| systems.get(&system_id).cloned())
}
pub(crate) fn set_active_camera(system_id: u8, component_id: u8) {
if let Ok(mut systems) = LATEST_UAS_SYSTEMS.lock() {
if let Some(system) = systems.get_mut(&system_id) {
system.active_camera_component = component_id;
}
}
}

View File

@@ -93,6 +93,8 @@ impl UdpClient {
lazy_static! { lazy_static! {
static ref UDP_CLIENT: Arc<Mutex<Option<UdpClient>>> = Arc::new(Mutex::new(None)); static ref UDP_CLIENT: Arc<Mutex<Option<UdpClient>>> = Arc::new(Mutex::new(None));
static ref LRF_CLIENT: Arc<Mutex<Option<UdpClient>>> = Arc::new(Mutex::new(None));
static ref COT_CLIENT: Arc<Mutex<Option<UdpClient>>> = Arc::new(Mutex::new(None));
} }
pub fn start(ctx: Context, address: String) -> &'static str { pub fn start(ctx: Context, address: String) -> &'static str {
@@ -125,13 +127,22 @@ pub fn start(ctx: Context, address: String) -> &'static str {
"Starting UDP Client" "Starting UDP Client"
} }
pub fn send_payload(ctx: Context, payload: String) -> &'static str { fn send_with_client(
if let Some(ref client) = *UDP_CLIENT.lock().unwrap() { client_slot: &Arc<Mutex<Option<UdpClient>>>,
ctx: Context,
payload: String,
missing_message: &'static str,
) {
if let Some(ref client) = *client_slot.lock().unwrap() {
client.send_payload(ctx, payload); client.send_payload(ctx, payload);
} else { } else {
let _ = ctx.callback_null("UDP SOCKET ERROR", "UDP Socket is not running"); let _ = ctx.callback_null("UDP SOCKET ERROR", missing_message);
info!("UDP send requested while socket was not running"); info!("UDP send requested while target socket was not running");
} }
}
pub fn send_payload(ctx: Context, payload: String) -> &'static str {
send_with_client(&UDP_CLIENT, ctx, payload, "UDP Socket is not running");
"Sending payload to UDP server" "Sending payload to UDP server"
} }
@@ -146,6 +157,116 @@ pub fn send_gps_cot(
"Sending GPS Cursor Over Time to UDP server" "Sending GPS Cursor Over Time to UDP server"
} }
pub fn send_eud_cot(
ctx: Context,
cursor_over_time: cot::gps::ExternalPositionPayload,
) -> &'static str {
send_with_client(
&COT_CLIENT,
ctx,
cursor_over_time.to_cot().convert_to_xml(),
"CoT UDP Socket is not running",
);
"Sending EUD Cursor Over Time to CoT UDP server"
}
pub fn start_lrf(ctx: Context, address: String) -> &'static str {
info!("LRF UDP socket start requested for {}", address);
let (tx, rx): (Sender<UdpCommand>, Receiver<UdpCommand>) = mpsc::channel();
let client = UdpClient {
tx,
address: address.clone(),
};
{
let mut client_guard = LRF_CLIENT.lock().unwrap();
if let Some(ref existing_client) = *client_guard {
info!(
"Stopping previous LRF UDP client {} before starting {}",
existing_client.address, address
);
existing_client.stop();
}
*client_guard = Some(UdpClient {
tx: client.tx.clone(),
address: client.address.clone(),
});
}
client.start(address, rx, ctx);
"Starting LRF UDP Client"
}
pub fn start_cot(ctx: Context, address: String) -> &'static str {
info!("CoT UDP socket start requested for {}", address);
let (tx, rx): (Sender<UdpCommand>, Receiver<UdpCommand>) = mpsc::channel();
let client = UdpClient {
tx,
address: address.clone(),
};
{
let mut client_guard = COT_CLIENT.lock().unwrap();
if let Some(ref existing_client) = *client_guard {
info!(
"Stopping previous CoT UDP client {} before starting {}",
existing_client.address, address
);
existing_client.stop();
}
*client_guard = Some(UdpClient {
tx: client.tx.clone(),
address: client.address.clone(),
});
}
client.start(address, rx, ctx);
"Starting CoT UDP Client"
}
pub fn send_lrf(ctx: Context, payload: cot::lrf::LaserRangeFinderPayload) -> &'static str {
send_with_client(
&LRF_CLIENT,
ctx,
payload.to_lrf_message(),
"LRF UDP Socket is not running",
);
"Sending Laser Range Finder payload to UDP server"
}
pub fn clear_lrf(ctx: Context, payload: cot::lrf::LaserRangeFinderClearPayload) -> &'static str {
send_with_client(
&LRF_CLIENT,
ctx,
payload.to_lrf_message(),
"LRF UDP Socket is not running",
);
"Clearing Laser Range Finder payload on UDP server"
}
pub fn send_digital_pointer_cot(
ctx: Context,
payload: cot::digital_pointer::DigitalPointerPayload,
) -> &'static str {
send_with_client(
&COT_CLIENT,
ctx,
payload.to_cot().convert_to_xml(),
"CoT UDP Socket is not running",
);
"Sending Digital Pointer CoT to UDP server"
}
pub fn stop(ctx: Context) -> &'static str { pub fn stop(ctx: Context) -> &'static str {
if let Some(ref client) = *UDP_CLIENT.lock().unwrap() { if let Some(ref client) = *UDP_CLIENT.lock().unwrap() {
info!("UDP socket stop requested for {}", client.address); info!("UDP socket stop requested for {}", client.address);
@@ -156,5 +277,15 @@ pub fn stop(ctx: Context) -> &'static str {
info!("UDP stop requested while socket was not running"); info!("UDP stop requested while socket was not running");
} }
if let Some(ref client) = *LRF_CLIENT.lock().unwrap() {
info!("LRF UDP socket stop requested for {}", client.address);
client.stop();
}
if let Some(ref client) = *COT_CLIENT.lock().unwrap() {
info!("CoT UDP socket stop requested for {}", client.address);
client.stop();
}
"Stopping UDP Client" "Stopping UDP Client"
} }

View File

@@ -70,7 +70,10 @@ pub fn start_stream(
let rtsp_url = if username.is_empty() || password.is_empty() { let rtsp_url = if username.is_empty() || password.is_empty() {
format!("rtsp://{}:{}/{}", address, port, stream_path) format!("rtsp://{}:{}/{}", address, port, stream_path)
} else { } else {
format!("rtsp://{}:{}@{}:{}/{}", username, password, address, port, stream_path) format!(
"rtsp://{}:{}@{}:{}/{}",
username, password, address, port, stream_path
)
}; };
let mut cmd = Command::new("ffmpeg"); let mut cmd = Command::new("ffmpeg");