34 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
671e7d5dc1 linted uas callsing functions 2026-05-07 05:02:51 -03:00
0ebd192487 fixed URI Stream sharing 2026-05-07 04:35:52 -03:00
3fc54a1fb5 test log 2026-05-07 04:13:20 -03:00
eaf38a4d06 Updated some mods because yes 2026-05-07 03:53:42 -03:00
6376b7acf0 Refacted UDP Socket config and created UAS addon 2026-05-07 03:53:19 -03:00
52edf94b17 refactored mavlink mocking to "uas" module on the extension 2026-05-07 03:37:22 -03:00
3e11dd9e16 updated data extract functions for mavlink integration 2026-05-05 12:20:12 -03:00
a9f09b6ce6 Added extension piece of code for handling the mavlink mocker 2026-05-05 12:19:14 -03:00
a43aa60f45 fixed side parser on role handler 2026-05-05 12:18:23 -03:00
9cba642e9b added mavlink indexing functions 2026-05-05 08:03:26 -03:00
c7494da901 added mavlink cot parsing functions 2026-05-05 08:03:01 -03:00
b9e848d66e Updated client side dialog to include mavlink port input 2026-05-05 08:02:38 -03:00
99f8d991be Added MavLink mocked drone support as client side feature 2026-05-05 07:48:54 -03:00
152 changed files with 6565 additions and 9875 deletions

3
.gitignore vendored
View File

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

View File

@@ -38,28 +38,7 @@ preset = "Hemtt"
[hemtt.launch.default] [hemtt.launch.default]
workshop = [ workshop = [
"450814997", # CBA_A3 "450814997", # CBA_A3
"463939057", # ACE "463939057", # ace
"751965892", # ACRE2
"2522638637", # ACE Extended Arsenal
"333310405", # Enhanced Movement
"2034363662", # Enhanced Movement Rework
"2941986336", # Hatchet Interaction Framework - Stable Version
"1745501605", # Hatchet H-60 pack - Stable Version
"843577117", # RHSUSAF
"843425103", # RHSAFRF
"843632231", # RHSSAF
"843593391", # RHSGREF
"1673456286", # 3CB Factions
"623475643", # 3den Enhanced
"2257686620", # Blastcore Murr Edition
"583496184", # CUP Terrains - Core
"3078351739", # Kunduz River
"1858075458", # LAMBS_Danger.fsm
"1808238502", # LAMBS_Suppression
"3425368881", # M4A1_URGI
"2268351256", # Tier One Weapons
"2560276469", # Restrict Markers
"3493557838" # Ballad of the Green Berets
] ]
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

@@ -10,44 +10,97 @@ class armatak_udp_socket_start_dialog {
class armatak_gui_module_udp_socket_dialog_main_frame: RscBackground { class armatak_gui_module_udp_socket_dialog_main_frame: RscBackground {
idc = 16960; idc = 16960;
x = "0.386562 * safezoneW + safezoneX"; x = "0.386562 * safezoneW + safezoneX";
y = "0.401 * safezoneH + safezoneY"; y = "0.357 * safezoneH + safezoneY";
w = "0.216563 * safezoneW"; w = "0.216563 * safezoneW";
h = "0.242 * safezoneH"; h = "0.495 * safezoneH";
colorBackground[]={0,0,0,0.45}; colorBackground[]={0,0,0,0.45};
}; };
}; };
class Controls { class Controls {
class armatak_gui_module_udp_socket_dialog_address_edit: RscEdit { class armatak_gui_module_udp_socket_dialog_address_edit: RscEdit {
idc = 16961; idc = 16961;
text = "168.15.0.3"; text = "192.168.15.121";
x = "0.391719 * safezoneW + safezoneX"; x = "0.391719 * safezoneW + safezoneX";
y = "0.445 * safezoneH + safezoneY"; y = "0.401 * 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};
}; };
class armatak_gui_module_udp_socket_dialog_address_port_edit: RscEdit { class armatak_gui_module_udp_socket_dialog_gnss_port_edit: RscEdit {
idc = 16962; idc = 16962;
text = "4349"; text = "4349";
x = "0.391719 * safezoneW + safezoneX"; x = "0.391719 * safezoneW + safezoneX";
y = "0.522 * safezoneH + safezoneY"; y = "0.478 * 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};
}; };
class armatak_gui_module_udp_socket_dialog_mavlink_port_edit: RscEdit {
idc = 16967;
text = "14550";
x = "0.391719 * safezoneW + safezoneX";
y = "0.555 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[]={0,0,0,0.5};
};
class armatak_gui_module_udp_socket_dialog_video_feed_url_edit: RscEdit {
idc = 16969;
text = "";
x = "0.391719 * safezoneW + safezoneX";
y = "0.709 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[]={0,0,0,0.5};
tooltip = "Optional shared feed URL. If empty, the UAV 3DEN URL is used first, then a local RTP fallback.";
};
class armatak_gui_module_udp_socket_dialog_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 = "Phone's Socket Local Address"; text = "EUD's Address";
x = "0.391719 * safezoneW + safezoneX"; x = "0.391719 * safezoneW + safezoneX";
y = "0.412 * safezoneH + safezoneY"; y = "0.368 * safezoneH + safezoneY";
w = "0.20625 * safezoneW"; w = "0.20625 * safezoneW";
h = "0.033 * safezoneH"; h = "0.033 * safezoneH";
}; };
class armatak_gui_module_udp_socket_dialog_address_port_text: RscText { class armatak_gui_module_udp_socket_dialog_gnss_port_text: RscText {
idc = 16964; idc = 16964;
text = "Phone's Socket Local Port"; text = "Network GNSS Port";
x = "0.391719 * safezoneW + safezoneX"; x = "0.391719 * safezoneW + safezoneX";
y = "0.489 * safezoneH + safezoneY"; y = "0.445 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.033 * safezoneH";
};
class armatak_gui_module_udp_socket_dialog_mavlink_port_text: RscText {
idc = 16968;
text = "Mavlink Port";
x = "0.391719 * safezoneW + safezoneX";
y = "0.522 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.033 * safezoneH";
};
class armatak_gui_module_udp_socket_dialog_video_feed_url_text: RscText {
idc = 16970;
text = "Video Feed URL (Optional)";
x = "0.391719 * safezoneW + safezoneX";
y = "0.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";
w = "0.20625 * safezoneW"; w = "0.20625 * safezoneW";
h = "0.033 * safezoneH"; h = "0.033 * safezoneH";
}; };
@@ -56,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.577 * safezoneH + safezoneY"; y = "0.786 * safezoneH + safezoneY";
w = "0.0464063 * safezoneW"; w = "0.0464063 * safezoneW";
h = "0.055 * safezoneH"; h = "0.055 * safezoneH";
}; };
@@ -65,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.577 * 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

@@ -2,7 +2,7 @@
params ["_logic"]; params ["_logic"];
_socket_is_running = player getVariable [QGVAR(eudConnected), false]; private _socket_is_running = player getVariable [QGVAR(eudConnected), false];
if (_socket_is_running) exitWith { if (_socket_is_running) exitWith {
["Socket is already running", "error", "UDP Socket"] call EFUNC(main,notify); ["Socket is already running", "error", "UDP Socket"] call EFUNC(main,notify);
@@ -11,21 +11,55 @@ if (_socket_is_running) exitWith {
disableSerialization; disableSerialization;
_udp_socket_instance_address = ctrlText 16961; private _eud_address = ctrlText 16961;
_udp_socket_instance_port = ctrlText 16962; private _gnss_port = ctrlText 16962;
private _mavlink_port = ctrlText 16967;
private _lrf_port = ctrlText 16971;
private _video_feed_url = ctrlText 16969;
_udp_socket_fulladdress = ((_udp_socket_instance_address) + ":" + (_udp_socket_instance_port)); private _udp_socket_fulladdress = _eud_address + ":" + _gnss_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(eudConnected), true]; 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(eudConnected), true, true];
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]];
if (_lrf_enabled) then {
"armatak" callExtension ["udp_socket:start_lrf", [_lrf_address]];
};
private _mdnsInstanceName = format ["ArmaTAK-%1", name player];
"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, 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);
[{ [{
if (player getVariable [QGVAR(eudConnected), false]) then { if !(player getVariable [QGVAR(eudConnected), false]) exitWith {};
"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

@@ -78,12 +78,12 @@ class Cfg3den {
condition = "objectVehicle"; condition = "objectVehicle";
typeName = "STRING"; typeName = "STRING";
}; };
class armatak_attribute_video_url { class armatak_attribute_marker_video_url {
displayName = "Video URL (RTSP)"; displayName = "Video Feed URL";
tooltip = "RTSP stream URL for UAS Tool integration. When set, the drone will appear in the ATAK UAS Tool with FOV cone and video feed. Format: rtsp://address:port/path (e.g. rtsp://192.168.1.10:8554/live/drone1). Leave empty to disable UAS Tool integration for this entity."; tooltip = "Shared UAV video URL. This per-vehicle value overrides the optional session video URL from Connect to EUD.";
property = "armatak_attribute_video_url"; property = "armatak_attribute_marker_video_url";
control = "Edit"; control = "Edit";
expression = "_this setVariable ['armatak_attribute_video_url',_value]"; expression = "_this setVariable ['armatak_attribute_marker_video_url',_value]";
defaultValue = "''"; defaultValue = "''";
condition = "objectVehicle"; condition = "objectVehicle";
typeName = "STRING"; typeName = "STRING";

View File

@@ -19,12 +19,45 @@ 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 {
file = "\armatak\armatak\addons\main\functions\api\fn_send_uas_platform_cot.sqf";
};
class send_uas_video_cot { class send_uas_video_cot {
file = "\armatak\armatak\addons\main\functions\api\fn_send_uas_video_cot.sqf"; file = "\armatak\armatak\addons\main\functions\api\fn_send_uas_video_cot.sqf";
}; };
class send_uas_sensor_cot { class send_uas_sensor_cot {
file = "\armatak\armatak\addons\main\functions\api\fn_send_uas_sensor_cot.sqf"; file = "\armatak\armatak\addons\main\functions\api\fn_send_uas_sensor_cot.sqf";
}; };
class set_uas_camera_override {
file = "\armatak\armatak\addons\main\functions\api\fn_set_uas_camera_override.sqf";
};
class stop_tcp_socket { class stop_tcp_socket {
file = "\armatak\armatak\addons\main\functions\api\fn_stop_tcp_socket.sqf"; file = "\armatak\armatak\addons\main\functions\api\fn_stop_tcp_socket.sqf";
}; };
@@ -37,12 +70,18 @@ class CfgFunctions {
class extract_marker_callsign { class extract_marker_callsign {
file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_marker_callsign.sqf"; file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_marker_callsign.sqf";
}; };
class extract_marker_video_url {
file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_marker_video_url.sqf";
};
class extract_role { class extract_role {
file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_role.sqf"; file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_role.sqf";
}; };
class extract_sensor_data { class extract_sensor_data {
file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_sensor_data.sqf"; file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_sensor_data.sqf";
}; };
class extract_uas_camera_data {
file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_uas_camera_data.sqf";
};
class extract_unit_callsign { class extract_unit_callsign {
file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_unit_callsign.sqf"; file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_unit_callsign.sqf";
}; };
@@ -128,6 +167,9 @@ class CfgFunctions {
class convert_to_rut_mandol { class convert_to_rut_mandol {
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_rut_mandol.sqf"; file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_rut_mandol.sqf";
}; };
class convert_to_hellanmaa {
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_hellanmaa.sqf";
};
}; };
}; };
}; };

View File

@@ -9,10 +9,14 @@ 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);
"armatak" callExtension ["uas:stop_endpoint", []];
"armatak" callExtension ["mdns:stop", []];
}; };
default {}; default {};
}; };
@@ -24,11 +28,55 @@ 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);
"armatak" callExtension ["uas:stop_endpoint", []];
"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);
"armatak" callExtension ["uas:stop_endpoint", []];
"armatak" callExtension ["mdns:stop", []];
};
};
case "MAVLINK UDP ERROR": {
_message = _function;
if (_data isNotEqualTo "") then {
_message = format ["%1: %2", _function, _data];
};
[_message, "warning", _name] call FUNC(notify);
};
case "MAVLINK UDP": {
private _history = missionNamespace getVariable ["armatak_uav_mavlink_callback_history", []];
_history pushBack [diag_tickTime, _function, _data];
if ((count _history) > 50) then {
_history deleteRange [0, (count _history) - 50];
};
missionNamespace setVariable ["armatak_uav_mavlink_callback_history", _history];
missionNamespace setVariable ["armatak_uav_last_mavlink_callback", [diag_tickTime, _function, _data]];
switch (_function) do {
case "COMMAND_LONG";
case "COMMAND_INT";
case "COMMAND_ACK";
case "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": {
"armatak" callExtension ["log", [["info", format ["MAVLINK UDP CALLBACK %1 %2", _function, _data]]]];
[_function, _data] call EFUNC(uav,handleMavlinkCallback);
};
default {};
}; };
}; };
case "TCP SOCKET": { case "TCP SOCKET": {
@@ -45,6 +93,12 @@ addMissionEventHandler ["ExtensionCallback", {
case "VIDEO": { case "VIDEO": {
[_function, "success", _name] call FUNC(notify); [_function, "success", _name] call FUNC(notify);
}; };
case "MDNS": {
[_function, "success", _name] call FUNC(notify);
};
case "MDNS ERROR": {
[_function, "warning", _name] call FUNC(notify);
};
case "VIDEO ERROR": { case "VIDEO ERROR": {
[_function, "error", _name] call FUNC(notify); [_function, "error", _name] call FUNC(notify);

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

@@ -4,34 +4,4 @@
params["_drone"]; params["_drone"];
private _atak_role = "a-f-A"; [_drone] call armatak_fnc_send_uas_platform_cot;
private _atak_callsign = [_drone] call armatak_fnc_extract_marker_callsign;
switch (side _drone) do {
case "WEST": {
_atak_role = "a-f-A-M-F-Q"
};
case "EAST": {
_atak_role = "a-h-A-M-F-Q"
};
case "INDEPENDENT": {
_atak_role = "a-n-A-M-F-Q"
};
case "CIVILIAN": {
_atak_role = "a-f-A-C"
};
default {
_atak_role = "a-f-A-M-F-Q"
};
};
_pre_defined_role = _drone getVariable "_atak_group_role";
if (!isNil "_pre_defined_role") then {
_callsign = _pre_defined_role;
};
_cot = [_drone, _atak_role, _atak_callsign] call armatak_fnc_send_marker_cot;
[_drone] call armatak_fnc_send_uas_video_cot;
[_drone] call armatak_fnc_send_uas_sensor_cot;

View File

@@ -0,0 +1,77 @@
params ["_drone"];
private _uuid = _drone call armatak_fnc_extract_uuid;
private _uavControl = UAVControl _drone;
private _controller = _uavControl param [0, objNull];
private _controller_uid = if (!isNull _controller) then { [_controller] call armatak_fnc_extract_uuid } else { _drone getVariable ["armatak_uas_controller_uid", _uuid] };
private _callsign = [_drone] call armatak_fnc_extract_marker_callsign;
private _video_url = [_drone] call armatak_fnc_extract_marker_video_url;
private _atak_role = "a-f-A-M-H-Q";
switch (side _drone) do {
case west: {
_atak_role = "a-f-A-M-H-Q";
};
case east: {
_atak_role = "a-h-A-M-H-Q";
};
case independent: {
_atak_role = "a-n-A-M-H-Q";
};
case civilian: {
_atak_role = "a-f-A-C";
};
default {
_atak_role = "a-f-A-M-H-Q";
};
};
private _position = _drone call armatak_client_fnc_extractClientPosition;
private _lat = _position select 1;
private _lon = _position select 2;
private _hae = _position select 3;
private _course = _position select 5;
private _speed = _position select 6;
private _cameraData = [_drone] call armatak_fnc_extract_uas_camera_data;
private _azimuth = _cameraData select 0;
private _elevation = _cameraData select 1;
private _fov = _cameraData select 2;
private _range = _cameraData select 3;
private _vfov = _drone getVariable ["armatak_uas_vfov", _fov];
private _yaw = round (getDir _drone);
private _pitch = (vectorDir _drone) select 2;
private _roll = (vectorUp _drone) select 0;
private _isFlying = parseNumber (isEngineOn _drone);
private _hal = ((getPosATL _drone) select 2) max 0;
private _vehicleType = if (_video_url == "") then {
typeOf _drone
} else {
format ["%1|armatak_video_url=%2", typeOf _drone, _video_url]
};
private _payload = [
_uuid,
_atak_role,
_callsign,
_lat,
_lon,
_hae,
_course,
_speed,
_azimuth,
_elevation,
_fov,
_vfov,
_range,
_yaw,
_pitch,
_roll,
_hal,
_vehicleType,
_isFlying,
_controller_uid
];
"armatak" callExtension ["tcp_socket:cot:uas_platform", [_payload]];

View File

@@ -1,55 +1,22 @@
// function name: armatak_fnc_send_uas_sensor_cot
// function author: Valmo / ArmaTAK contributors
// function description:
// Sends a b-m-p-s-p-loc CoT event every router tick (1 s) for a drone.
// This is the "sensor position" event consumed by the ATAK UAS Tool to:
// - Draw the FOV cone on the moving map.
// - Compute four-corners for AR marker overlay on the video feed.
// - Show the SPoI (Sensor Point of Interest) crosshair.
//
// The event references the drone's b-i-v video endpoint via the drone UUID,
// so armatak_fnc_send_uas_video_cot must also be called for the same drone.
//
// Exits silently when "armatak_attribute_video_url" is not set, which keeps
// the behavior identical to the old fn_send_drone_cot for drones without a
// configured video stream.
//
// Arguments:
// 0: _drone <OBJECT> The drone object.
//
// Return value: none
params ["_drone"]; params ["_drone"];
private _video_url = _drone getVariable ["armatak_attribute_video_url", ""]; private _video_url = [_drone] call armatak_fnc_extract_marker_video_url;
if (_video_url == "") exitWith {}; if (_video_url == "") exitWith {};
private _uuid = _drone call armatak_fnc_extract_uuid; private _uuid = _drone call armatak_fnc_extract_uuid;
private _video_uid = _uuid + "-video";
private _sensor_uid = _uuid + "-sensor"; private _sensor_uid = _uuid + "-sensor";
private _callsign = [_drone] call armatak_fnc_extract_marker_callsign; private _callsign = [_drone] call armatak_fnc_extract_marker_callsign;
private _pos = (getPos _drone) call armatak_client_fnc_convertClientLocation; private _position = _drone call armatak_client_fnc_extractClientPosition;
private _lat = _pos select 0; private _lat = _position select 1;
private _lon = _pos select 1; private _lon = _position select 2;
private _hae = _pos select 2; private _hae = _position select 3;
private _azimuth = parseNumber ((getDir _drone) toFixed 0); private _cameraData = [_drone] call armatak_fnc_extract_uas_camera_data;
private _azimuth = _cameraData select 0;
private _fov = _cameraData select 2;
private _range = _cameraData select 3;
private _allTurrets = [_drone, false] call BIS_fnc_allTurrets; private _payload = [_sensor_uid, _video_uid, _callsign, _lat, _lon, _hae, _azimuth, _fov, _range];
if (count _allTurrets > 0) then {
private _firstTurretPath = _allTurrets select 0;
private _turretWeapons = _drone weaponsTurret _firstTurretPath;
if (_turretWeapons isNotEqualTo []) then {
private _tDir = _drone weaponDirection (_turretWeapons select 0);
if (!((_tDir select 0) == 0 && (_tDir select 1) == 0)) then {
_azimuth = round (((_tDir select 0) atan2 (_tDir select 1) + 360) mod 360);
};
};
};
private _fov = _drone getVariable ["armatak_uas_fov", 60];
private _range = round (((getPosATL _drone) select 2) max 1);
private _payload = [_sensor_uid, _uuid, _callsign, _lat, _lon, _hae, _azimuth, _fov, _range];
"armatak" callExtension ["tcp_socket:cot:uas_sensor", [_payload]]; "armatak" callExtension ["tcp_socket:cot:uas_sensor", [_payload]];

View File

@@ -1,30 +1,20 @@
// function name: armatak_fnc_send_uas_video_cot
// function author: Valmo / ArmaTAK contributors
// function description:
// Sends a b-i-v CoT event that declares the RTSP video endpoint for a drone.
// The ATAK UAS Tool picks this up and shows the drone in its UAS list with
// the associated video feed available for playback.
//
// The drone entity MUST have the variable "armatak_attribute_video_url" set
// to a valid RTSP URL, e.g.:
// _drone setVariable ["armatak_attribute_video_url", "rtsp://192.168.1.10:8554/live/drone1"];
// or via the 3DEN attribute "Video URL (RTSP)" in the ARMA Team Awareness Kit
// attribute category.
//
// If the variable is absent or empty the function exits silently.
//
// Arguments:
// 0: _drone <OBJECT> The drone object.
//
// Return value: none
params ["_drone"]; params ["_drone"];
private _video_url = _drone getVariable ["armatak_attribute_video_url", ""]; private _video_url = [_drone] call armatak_fnc_extract_marker_video_url;
if (_video_url == "") exitWith {}; if (_video_url == "") exitWith {};
private _uuid = _drone call armatak_fnc_extract_uuid; private _uuid = _drone call armatak_fnc_extract_uuid;
private _video_uid = _uuid + "-video";
private _callsign = [_drone] call armatak_fnc_extract_marker_callsign; private _callsign = [_drone] call armatak_fnc_extract_marker_callsign;
private _payload = [_uuid, _callsign, _video_url]; private _signature = format ["%1|%2|%3", _video_uid, _callsign, _video_url];
private _nextRefreshAt = _drone getVariable ["armatak_next_uas_video_refresh_at", 0];
private _lastSignature = _drone getVariable ["armatak_last_uas_video_signature", ""];
if (_signature == _lastSignature && {diag_tickTime < _nextRefreshAt}) exitWith {};
_drone setVariable ["armatak_last_uas_video_signature", _signature, false];
_drone setVariable ["armatak_next_uas_video_refresh_at", diag_tickTime + 300, false];
private _payload = [_video_uid, _callsign, _video_url];
"armatak" callExtension ["tcp_socket:cot:uas_video", [_payload]]; "armatak" callExtension ["tcp_socket:cot:uas_video", [_payload]];

View File

@@ -0,0 +1,9 @@
params ["_drone", ["_cameraData", []]];
if (isNull _drone) exitWith {};
if ((_cameraData isEqualType []) && {(count _cameraData) >= 6}) then {
_drone setVariable ["armatak_uas_camera_data_override", _cameraData + [serverTime], false];
} else {
_drone setVariable ["armatak_uas_camera_data_override", nil, false];
};

View File

@@ -5,29 +5,45 @@
params["_unit"]; params["_unit"];
private _callsign = ""; private _callsign = "";
private _displayName = localize (getText (configOf _unit >> "displayName"));
private _markerCallsignOverride = _unit getVariable ["armatak_attribute_marker_callsign", ""];
if (_markerCallsignOverride isNotEqualTo "") exitWith {
_markerCallsignOverride
};
if (_displayName isEqualTo "") then {
_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 = getText (configOf _unit >> "displayName"); _callsign = [_displayName, _vehicleName] select (_vehicleName isNotEqualTo "");
if (!isNull driver _unit) then { if (!isNull driver _unit) then {
_callsign = getText (configOf _unit >> "displayName") + " | " + ([name (driver _unit)] call armatak_fnc_shorten_name); _callsign = _displayName + " | " + ([name (driver _unit)] call armatak_fnc_shorten_name);
}; };
}; };
if (unitIsUAV _unit) then { if (unitIsUAV _unit) then {
_callsign = getText (configOf _unit >> "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]";
} else { } else {
_callsign = (_callsign) + "[OFF]"; _callsign = _callsign + " [OFF]";
} }
}; };
armatak_attribute_marker_callsign = _unit getVariable "armatak_attribute_marker_callsign"; if (_callsign isEqualTo "") then {
_callsign = _displayName;
if (!isNil "armatak_attribute_marker_callsign" or armatak_attribute_marker_callsign != '') then {
_callsign = armatak_attribute_marker_callsign;
}; };
_callsign _callsign

View File

@@ -7,13 +7,7 @@ params["_unit"];
private _affiliation = "f"; private _affiliation = "f";
private _type = "G"; private _type = "G";
private _role = "a-f-G-U-C-I"; private _role = "a-f-G-U-C-I";
private _side = side _unit; private _side = _unit getVariable ["armatak_current_side", side _unit];
if (isNil {
_unit getVariable "armatak_current_side"
}) then {
_side = _unit getVariable "armatak_current_side";
};
switch (str _side) do { switch (str _side) do {
case "WEST": { case "WEST": {
@@ -33,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 {
@@ -77,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";
@@ -117,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,27 +1,32 @@
params["_unit"]; params["_unit"];
if (_unit getVariable ["armatak_disable_sensor_data", false]) exitWith {};
_target = getSensorTargets (_unit); _target = getSensorTargets (_unit);
{ {
_unit = _x select 0; private _targetUnit = _x select 0;
_position = _x select 1; _position = _x select 1;
_status = _x select 2; _status = _x select 2;
private _targetType = toLower (typeOf _targetUnit);
if ((_targetType find "lasertarget") < 0) then {
if (isNil { if (isNil {
_unit getVariable "armatak_current_side" _targetUnit getVariable "armatak_current_side"
}) then { }) then {
_unit setVariable ["armatak_current_side", side _unit]; _targetUnit setVariable ["armatak_current_side", side _targetUnit];
}; };
if (_status != "destroyed" && !(_unit in armatak_server_syncedUnits)) then { if (_status != "destroyed" && !(_targetUnit in armatak_server_syncedUnits)) then {
_unit_position = _unit call armatak_client_fnc_extractClientPosition; _unit_position = _targetUnit call armatak_client_fnc_extractClientPosition;
_uuid = _unit call armatak_fnc_extract_uuid; _uuid = _targetUnit call armatak_fnc_extract_uuid;
_type = _unit call armatak_fnc_extract_role; _type = _targetUnit call armatak_fnc_extract_role;
_callsign = getText (configOf _unit >> "displayName"); _callsign = getText (configOf _targetUnit >> "displayName");
_marker_cot = [_uuid, _type, _unit_position select 1, _unit_position select 2, _unit_position select 3, _callsign, _unit_position select 5, _unit_position select 6]; _marker_cot = [_uuid, _type, _unit_position select 1, _unit_position select 2, _unit_position select 3, _callsign, _unit_position select 5, _unit_position select 6];
"armatak" callExtension ["tcp_socket:cot:marker", [_marker_cot]]; "armatak" callExtension ["tcp_socket:cot:marker", [_marker_cot]];
}; };
};
} forEach _target; } forEach _target;

View File

@@ -0,0 +1,143 @@
params ["_drone", ["_cameraMode", "turret"]];
private _override = _drone getVariable ["armatak_uas_camera_data_override", []];
private _isLocalController = hasInterface && {!isNull player} && {(getConnectedUAV player) isEqualTo _drone};
if (!_isLocalController && {_override isEqualType []} && {(count _override) >= 7}) then {
private _updatedAt = _override param [6, -1000];
if ((time - _updatedAt) <= 5) exitWith {
private _overrideSpiAsl = _override param [4, []];
private _overrideSpiGeo = _override param [5, []];
_drone setVariable ["armatak_uas_spi_asl", _overrideSpiAsl, false];
_drone setVariable ["armatak_uas_spi_geo", _overrideSpiGeo, false];
_override select [0, 6]
};
};
private _defaultFov = _drone getVariable ["armatak_uas_fov", 60];
private _maxRange = _drone getVariable ["armatak_uas_max_range", 15000];
private _originASL = getPosASL _drone;
private _originAGL = ASLToAGL _originASL;
private _cameraDir = [];
private _spiASL = [];
private _slantRange = 0;
if (_cameraMode isNotEqualTo "fpv") then {
private _laserTarget = laserTarget _drone;
if (!isNull _laserTarget) then {
private _laserTargetWorld = getPosWorld _laserTarget;
private _laserTargetAslZ = (getPosASL _laserTarget) select 2;
_spiASL = [_laserTargetWorld select 0, _laserTargetWorld select 1, _laserTargetAslZ];
_cameraDir = _spiASL vectorDiff _originASL;
_slantRange = _originASL vectorDistance _spiASL;
};
};
if (_cameraDir isEqualTo [] && {_cameraMode isNotEqualTo "fpv"}) then {
private _uavControl = UAVControl _drone;
private _controlledTurretPath = _uavControl param [1, []];
private _candidateTurrets = [];
if ((_controlledTurretPath isEqualType []) && {_controlledTurretPath isNotEqualTo []}) then {
_candidateTurrets pushBack _controlledTurretPath;
};
{
if !(_x in _candidateTurrets) then {
_candidateTurrets pushBack _x;
};
} forEach (allTurrets _drone);
{
private _turretWeapons = _drone weaponsTurret _x;
if (_turretWeapons isNotEqualTo []) exitWith {
private _weapon = _turretWeapons select 0;
private _weaponDirection = _drone weaponDirection _weapon;
if (_weaponDirection isNotEqualTo [0, 0, 0]) then {
_cameraDir = _weaponDirection;
};
};
} forEach _candidateTurrets;
};
if (_cameraDir isEqualTo []) then {
_cameraDir = vectorDirVisual _drone;
};
private _dirMagnitude = vectorMagnitude _cameraDir;
if (_dirMagnitude <= 0) then {
private _fallbackAzimuth = getDir _drone;
_cameraDir = [sin _fallbackAzimuth, cos _fallbackAzimuth, -1];
_dirMagnitude = vectorMagnitude _cameraDir;
};
_cameraDir = _cameraDir vectorMultiply (1 / _dirMagnitude);
private _dirX = _cameraDir select 0;
private _dirY = _cameraDir select 1;
private _dirZ = _cameraDir select 2;
private _horizontalMagnitude = sqrt ((_dirX * _dirX) + (_dirY * _dirY));
private _azimuth = (((_dirX atan2 _dirY) + 360) mod 360);
private _elevation = (_dirZ atan2 (_horizontalMagnitude max 0.001));
if (_spiASL isEqualTo []) then {
private _altitudeAGL = (_originAGL select 2) max 0.1;
private _probeASL = _originASL vectorAdd (_cameraDir vectorMultiply _maxRange);
if (_dirZ < -0.01 && {terrainIntersectASL [_originASL, _probeASL]}) then {
private _near = _originASL;
private _far = _probeASL;
for "_i" from 0 to 24 do {
private _mid = [
((_near select 0) + (_far select 0)) / 2,
((_near select 1) + (_far select 1)) / 2,
((_near select 2) + (_far select 2)) / 2
];
if (terrainIntersectASL [_originASL, _mid]) then {
_far = _mid;
} else {
_near = _mid;
};
};
_spiASL = _far;
_slantRange = _originASL vectorDistance _spiASL;
} else {
private _verticalComponent = abs _dirZ;
if (_verticalComponent > 0.01) then {
_slantRange = (_altitudeAGL / _verticalComponent) min _maxRange;
} else {
_slantRange = _maxRange;
};
_slantRange = _slantRange max 1;
_spiASL = _originASL vectorAdd (_cameraDir vectorMultiply _slantRange);
_spiASL set [2, getTerrainHeightASL [_spiASL select 0, _spiASL select 1]];
};
};
if (_slantRange <= 0) then {
_slantRange = (_originASL vectorDistance _spiASL) max 1;
};
private _spiAgl = ASLToAGL _spiASL;
private _spiWorld = [_spiAgl select 0, _spiAgl select 1, (_spiAgl select 2) max 0];
private _spiGeo = _spiWorld call armatak_client_fnc_convertClientLocation;
_drone setVariable ["armatak_uas_spi_asl", _spiASL, false];
_drone setVariable ["armatak_uas_spi_geo", _spiGeo, false];
[
round _azimuth,
round _elevation,
round _defaultFov,
round (_slantRange max 1),
_spiASL,
_spiGeo
]

View File

@@ -20,10 +20,10 @@ if (side _unit == east) then {
_callsign = getText (configOf _unit >> "displayName"); _callsign = getText (configOf _unit >> "displayName");
}; };
armatak_attribute_unit_callsign = _unit getVariable "armatak_attribute_unit_callsign"; private _unitCallsignOverride = _unit getVariable ["armatak_attribute_unit_callsign", ""];
if (!isNil "armatak_attribute_unit_callsign" or armatak_attribute_unit_callsign != '') then { if (_unitCallsignOverride isNotEqualTo "") then {
_callsign = armatak_attribute_unit_callsign; _callsign = _unitCallsignOverride;
}; };
_callsign _callsign

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;
@@ -25,23 +43,38 @@ GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
[_x, _callsign, _group_name, _group_role] call armatak_fnc_send_eud_cot; [_x, _callsign, _group_name, _group_role] call armatak_fnc_send_eud_cot;
[_x] call armatak_fnc_send_digital_pointer_cot; [_x] call armatak_fnc_send_digital_pointer_cot;
}; };
case (unitIsUAV _x): {
if !(_x getVariable ["armatak_uav_mavlink_broadcasting", false]) then {
_atak_type = [_x] call armatak_fnc_extract_role;
_callsign = [_x] call armatak_fnc_extract_marker_callsign;
[_x, _atak_type, _callsign] call armatak_fnc_send_drone_cot;
_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"): {
_atak_type = [_x] call armatak_fnc_extract_role; _atak_type = [_x] call armatak_fnc_extract_role;
_callsign = [_x] call armatak_fnc_extract_marker_callsign; _callsign = [_x] call armatak_fnc_extract_marker_callsign;
[_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 {
_atak_type = [_x] call armatak_fnc_extract_role; _atak_type = [_x] call armatak_fnc_extract_role;
_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

1
addons/uav/$PBOPREFIX$ Normal file
View File

@@ -0,0 +1 @@
armatak\armatak\addons\uav

View File

@@ -0,0 +1,17 @@
class Extended_PreStart_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
};
};
class Extended_PreInit_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
};
};
class Extended_PostInit_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_postInit));
};
};

6
addons/uav/XEH_PREP.hpp Normal file
View File

@@ -0,0 +1,6 @@
PREP(startMavlinkBroadcast);
PREP(stopMavlinkBroadcast);
PREP(updateMavlinkBroadcast);
PREP(resolveVideoUri);
PREP(handleMavlinkCallback);
PREP(parseMavlinkCallbackData);

View File

@@ -0,0 +1,5 @@
#include "script_component.hpp"
if (!hasInterface) exitWith {};
SETVAR(player,GVAR(mavlinkPFH),-1);

View File

@@ -0,0 +1,9 @@
#include "script_component.hpp"
ADDON = false;
PREP_RECOMPILE_START;
#include "XEH_PREP.hpp"
PREP_RECOMPILE_END;
ADDON = true;

View File

@@ -0,0 +1,3 @@
#include "script_component.hpp"
#include "XEH_PREP.hpp"

20
addons/uav/config.cpp Normal file
View File

@@ -0,0 +1,20 @@
#include "script_component.hpp"
class CfgPatches {
class ADDON {
name = COMPONENT_NAME;
units[] = {};
weapons[] = {};
requiredAddons[] = {
"cba_main",
"ace_main",
"armatak_main",
"armatak_client"
};
requiredVersion = REQUIRED_VERSION;
author = PROJECT_AUTHOR;
url = "https://github.com/valmojr/armatak";
};
};
#include "CfgEventHandlers.hpp"

View File

@@ -0,0 +1,382 @@
#include "..\script_component.hpp"
params ["_function", ["_data", "", [""]]];
if (!hasInterface) exitWith {};
private _payload = [_data] call FUNC(parseMavlinkCallbackData);
private _uav = getConnectedUAV player;
if (isNull _uav) then {
_uav = player getVariable [QGVAR(broadcastingUav), objNull];
};
if (isNull _uav) exitWith {
"armatak" callExtension ["log", [["warn", format ["Ignoring MAVLINK UDP callback %1 because no UAV is connected: %2", _function, _data]]]];
};
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 _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 {
case "COMMAND_LONG": {
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: {
private _doArm = (["param1", 0] call _number) >= 1;
_uav engineOn _doArm;
_uav setVariable ["armatak_uas_armed", _doArm, true];
if (_doArm) then {
_uav setFuel ((fuel _uav) max 0.1);
};
systemChat format ["ATAK %1 %2", ["DISARM", "ARM"] select _doArm, _callsign];
"armatak" callExtension ["log", [["info", format ["Applied MAVLINK ARM=%1 to UAV %2", _doArm, _uav]]]];
};
case 22: {
private _alt = (["param7", 75] call _number) max 10;
private _pos = getPosATL _uav;
_pos set [2, _alt];
[_uav, _pos, "MOVE", 80, 25] call _commandMove;
_uav setVelocityModelSpace [0, 15, 8];
systemChat format ["ATAK TAKEOFF %1m %2", round _alt, _callsign];
};
case 21: {
[9] call _applyMode;
};
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];
};
};
default {
"armatak" callExtension ["log", [["info", format ["Unhandled MAVLINK COMMAND_LONG %1 (%2): %3", _command, _commandName, _data]]]];
};
};
};
case "COMMAND_INT": {
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": {
"armatak" callExtension ["log", [["info", format ["Received MAVLINK COMMAND_ACK %1 (%2): %3", _command, _commandName, _data]]]];
};
case "MANUAL_CONTROL": {
"armatak" callExtension ["log", [["info", format ["Received MAVLINK MANUAL_CONTROL: %1", _data]]]];
};
default {
"armatak" callExtension ["log", [["info", format ["Unhandled MAVLINK UDP callback %1: %2", _function, _data]]]];
};
};

View File

@@ -0,0 +1,17 @@
#include "..\script_component.hpp"
params [["_raw", "", [""]]];
private _pairs = createHashMap;
{
private _entry = _x;
private _separatorIndex = _entry find "=";
if (_separatorIndex > 0) then {
private _key = _entry select [0, _separatorIndex];
private _value = _entry select [_separatorIndex + 1];
_pairs set [_key, _value];
};
} forEach (_raw splitString ";");
_pairs

View File

@@ -0,0 +1,45 @@
#include "..\script_component.hpp"
params [["_uav", objNull, [objNull]]];
private _defaultVideoUri = "rtsp://undefined:554/fpv";
private _activelyControlledUav = if (!isNull player) then {getConnectedUAV player} else {objNull};
private _normalize = {
params ["_rawUrl"];
private _url = trim _rawUrl;
if (_url isEqualTo "") exitWith {""};
if (_url find "://" >= 0) exitWith {_url};
if (_url find "/" >= 0) exitWith {
format ["rtsp://%1", _url]
};
format ["rtp://%1", _url]
};
if (!isNull _uav) then {
private _objectVideoUrl = [_uav] call armatak_fnc_extract_marker_video_url;
private _normalizedObjectVideoUrl = [_objectVideoUrl] call _normalize;
if (_normalizedObjectVideoUrl isNotEqualTo "") exitWith {
_normalizedObjectVideoUrl
};
private _activeSessionVideoUrl = player getVariable [QEGVAR(client,video_feed_url), ""];
private _normalizedActiveSessionVideoUrl = [_activeSessionVideoUrl] call _normalize;
if (_normalizedActiveSessionVideoUrl isNotEqualTo "") exitWith {
_normalizedActiveSessionVideoUrl
};
_defaultVideoUri
};
private _sessionVideoUrl = player getVariable [QEGVAR(client,video_feed_url), ""];
private _normalizedSessionVideoUrl = [_sessionVideoUrl] call _normalize;
if (_normalizedSessionVideoUrl isNotEqualTo "") exitWith {
_normalizedSessionVideoUrl
};
_defaultVideoUri

View File

@@ -0,0 +1,16 @@
#include "..\script_component.hpp"
if (!hasInterface) exitWith {};
private _existingPfh = player getVariable [QGVAR(mavlinkPFH), -1];
if (_existingPfh >= 0) then {
[_existingPfh] call CBA_fnc_removePerFrameHandler;
};
player setVariable [QGVAR(broadcastingUav), objNull];
private _pfh = [{
call FUNC(updateMavlinkBroadcast);
}, 0.5, []] call CBA_fnc_addPerFrameHandler;
player setVariable [QGVAR(mavlinkPFH), _pfh];

View File

@@ -0,0 +1,17 @@
#include "..\script_component.hpp"
if (!hasInterface) exitWith {};
private _existingPfh = player getVariable [QGVAR(mavlinkPFH), -1];
if (_existingPfh >= 0) then {
[_existingPfh] call CBA_fnc_removePerFrameHandler;
player setVariable [QGVAR(mavlinkPFH), -1];
};
private _broadcastingUav = player getVariable [QGVAR(broadcastingUav), objNull];
if (!isNull _broadcastingUav) then {
_broadcastingUav setVariable ["armatak_uav_mavlink_broadcasting", false, true];
systemChat "UAV broadcasting stopped";
};
player setVariable [QGVAR(broadcastingUav), objNull];

View File

@@ -0,0 +1,122 @@
#include "..\script_component.hpp"
private _broadcastingUav = player getVariable [QGVAR(broadcastingUav), objNull];
if !(player getVariable [QEGVAR(client,eudConnected), false]) exitWith {
if (!isNull _broadcastingUav) then {
_broadcastingUav setVariable ["armatak_uav_mavlink_broadcasting", false, true];
player setVariable [QGVAR(broadcastingUav), objNull];
};
};
private _uav = getConnectedUAV player;
if (isNull _uav) then {
_uav = _broadcastingUav;
};
if (isNull _uav || {!alive _uav}) exitWith {
if (!isNull _broadcastingUav) then {
_broadcastingUav setVariable ["armatak_uav_mavlink_broadcasting", false, true];
player setVariable [QGVAR(broadcastingUav), objNull];
systemChat "UAV broadcasting stopped";
"armatak" callExtension ["log", [["info", "UAV broadcasting stopped because the UAV is no longer available"]]];
};
};
if (_broadcastingUav isNotEqualTo _uav) then {
if (!isNull _broadcastingUav) then {
_broadcastingUav setVariable ["armatak_uav_mavlink_broadcasting", false, true];
};
player setVariable [QGVAR(broadcastingUav), _uav];
_uav setVariable ["armatak_uav_mavlink_broadcasting", true, true];
private _callsign = [_uav] call armatak_fnc_extract_marker_callsign;
systemChat format ["Broadcasting UAV %1", _callsign];
"armatak" callExtension ["log", [["info", format ["Broadcasting UAV %1 via MAVLink mock to %2", _callsign, player getVariable [QEGVAR(client,mavlink_address), ""]]]]];
};
_uav setVariable ["armatak_uav_mavlink_broadcasting", true, true];
private _mavlinkAddress = player getVariable [QEGVAR(client,mavlink_address), ""];
if (_mavlinkAddress isEqualTo "") exitWith {};
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 _callsign = [_uav] call armatak_fnc_extract_marker_callsign;
private _videoUri = [_uav] call FUNC(resolveVideoUri);
private _dir = vectorDir _uav;
private _up = vectorUp _uav;
private _yaw = getDir _uav;
private _pitch = asin (((_dir select 2) max -1) min 1);
private _roll = asin (((_up select 0) max -1) min 1);
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 _gimbalPitch = _pitch;
private _gimbalYaw = _yaw;
private _hfov = _uav getVariable ["armatak_uas_fov", 60];
private _vfov = _uav getVariable ["armatak_uas_vfov", (_hfov * 0.5625)];
private _imageLat = _pos select 1;
private _imageLon = _pos select 2;
private _imageAlt = _pos select 3;
private _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 []};
if (_cameraData isEqualType [] && {(count _cameraData) >= 6}) then {
_gimbalYaw = _cameraData param [0, _yaw];
_gimbalPitch = _cameraData param [1, _pitch];
_hfov = _cameraData param [2, _hfov];
_vfov = _uav getVariable ["armatak_uas_vfov", (_hfov * 0.5625)];
private _spiGeo = _cameraData param [5, []];
if (_spiGeo isEqualType [] && {(count _spiGeo) >= 3}) then {
_imageLat = _spiGeo select 0;
_imageLon = _spiGeo select 1;
_imageAlt = _spiGeo select 2;
};
};
private _systemPayload = [
_mavlinkAddress,
_uuid,
_callsign,
_uavType,
_pos select 1,
_pos select 2,
_pos select 3,
_relAlt,
_pos select 5,
_pos select 6,
_roll,
_pitch,
_yaw,
parseNumber _armed,
parseNumber _landed,
_gimbalRoll,
_gimbalPitch,
_gimbalYaw,
_videoUri,
_hfov,
_vfov,
_imageLat,
_imageLon,
_imageAlt,
parseNumber _hasTurretCamera,
_batteryRemaining
];
"armatak" callExtension ["uas:send_uas_system", [_systemPayload]];

View File

@@ -0,0 +1,17 @@
#define COMPONENT uav
#define COMPONENT_BEAUTIFIED UAV
#include "\armatak\armatak\addons\main\script_mod.hpp"
// #define DEBUG_MODE_FULL
// #define DISABLE_COMPILE_CACHE
// #define ENABLE_PERFORMANCE_COUNTERS
#ifdef DEBUG_ENABLED_UAV
#define DEBUG_MODE_FULL
#endif
#ifdef DEBUG_SETTINGS_UAV
#define DEBUG_SETTINGS DEBUG_SETTINGS_UAV
#endif
#include "\z\ace\addons\main\script_macros.hpp"

View File

@@ -1,3 +1,4 @@
use super::video::video_detail_xml;
use chrono::{Duration, SecondsFormat, Utc}; use chrono::{Duration, SecondsFormat, Utc};
use uuid::Uuid; use uuid::Uuid;
@@ -17,17 +18,10 @@ 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 {
fn escape_xml_attribute(value: &str) -> String {
value
.replace('&', "&amp;")
.replace('"', "&quot;")
.replace('<', "&lt;")
.replace('>', "&gt;")
}
pub fn convert_to_xml(&self) -> String { pub fn convert_to_xml(&self) -> String {
let uuid = match &self.uuid { let uuid = match &self.uuid {
Some(uuid) => uuid, Some(uuid) => uuid,
@@ -41,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();
@@ -118,13 +113,7 @@ impl CursorOverTime {
if let Some(video_url) = &self.video_url { if let Some(video_url) = &self.video_url {
if !video_url.trim().is_empty() { if !video_url.trim().is_empty() {
xml.push_str( xml.push_str(&video_detail_xml(video_url, uuid, &self.contact_callsign));
format!(
"<__video url=\"{}\" />",
Self::escape_xml_attribute(video_url.trim())
)
.as_str(),
);
} }
} }

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,8 +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;

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

@@ -1,59 +1,208 @@
// src/cot/uas.rs use super::video::video_detail_xml;
//
// CoT types required for ATAK UAS Tool integration.
//
// Two event types are needed so that the UAS Tool plugin recognises a drone:
//
// b-i-v — Video endpoint declaration. Tells the UAS Tool where
// to pull the RTSP stream for this drone.
//
// b-m-p-s-p-loc — Sensor position event. Carries the camera azimuth,
// field-of-view, and slant-range that the UAS Tool uses
// to draw the FOV cone on the map and to project AR
// markers onto the video feed.
//
// The two events are linked: the b-m-p-s-p-loc detail contains
// <__video uid="<drone-uuid>"/>
// which references the uid of the b-i-v event, so the UAS Tool knows which
// video stream belongs to this sensor.
use arma_rs::{FromArma, FromArmaError}; use arma_rs::{FromArma, FromArmaError};
use chrono::{Duration, SecondsFormat, Utc}; use chrono::{Duration, SecondsFormat, Utc};
// --------------------------------------------------------------------------- fn escape_xml(value: &str) -> String {
// Helpers value
// --------------------------------------------------------------------------- .replace('&', "&amp;")
.replace('"', "&quot;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('\'', "&apos;")
}
/// Parse an RTSP URL of the form rtsp://address:port/path
/// into its three components.
fn parse_rtsp_url(url: &str) -> Option<(String, String, String)> { fn parse_rtsp_url(url: &str) -> Option<(String, String, String)> {
let without_proto = url.strip_prefix("rtsp://")?; let without_proto = url.strip_prefix("rtsp://")?;
let slash_pos = without_proto.find('/')?; let slash_pos = without_proto.find('/')?;
let host_port = &without_proto[..slash_pos]; let host_port = &without_proto[..slash_pos];
let path = &without_proto[slash_pos..]; // includes the leading '/' let path = &without_proto[slash_pos..];
let colon_pos = host_port.rfind(':')?; let colon_pos = host_port.rfind(':')?;
let address = host_port[..colon_pos].to_string(); let address = host_port[..colon_pos].to_string();
let port = host_port[colon_pos + 1..].to_string(); let port = host_port[colon_pos + 1..].to_string();
Some((address, port, path.to_string())) Some((address, port, path.to_string()))
} }
// --------------------------------------------------------------------------- pub struct UasPlatformCoTPayload {
// b-i-v Video endpoint declaration pub uid: String,
// --------------------------------------------------------------------------- pub cot_type: String,
pub callsign: String,
pub point_lat: f64,
pub point_lon: f64,
pub point_hae: f32,
pub track_course: i32,
pub track_speed: f32,
pub sensor_azimuth: i32,
pub sensor_elevation: i32,
pub sensor_fov: i32,
pub sensor_vfov: i32,
pub sensor_range: i32,
pub attitude_yaw: i32,
pub attitude_pitch: f32,
pub attitude_roll: f32,
pub hal: f32,
pub vehicle_type_tag: String,
pub is_flying: i32,
pub link_uid: String,
}
impl FromArma for UasPlatformCoTPayload {
fn from_arma(data: String) -> Result<UasPlatformCoTPayload, FromArmaError> {
let (
uid,
cot_type,
callsign,
point_lat,
point_lon,
point_hae,
track_course,
track_speed,
sensor_azimuth,
sensor_elevation,
sensor_fov,
sensor_vfov,
sensor_range,
attitude_yaw,
attitude_pitch,
attitude_roll,
hal,
vehicle_type_tag,
is_flying,
link_uid,
) = <(
String,
String,
String,
f64,
f64,
f32,
i32,
f32,
i32,
i32,
i32,
i32,
i32,
i32,
f32,
f32,
f32,
String,
i32,
String,
)>::from_arma(data)?;
Ok(Self {
uid,
cot_type,
callsign,
point_lat,
point_lon,
point_hae,
track_course,
track_speed,
sensor_azimuth,
sensor_elevation,
sensor_fov,
sensor_vfov,
sensor_range,
attitude_yaw,
attitude_pitch,
attitude_roll,
hal,
vehicle_type_tag,
is_flying,
link_uid,
})
}
}
impl UasPlatformCoTPayload {
pub fn to_xml(&self) -> String {
let uid = escape_xml(&self.uid);
let cot_type = escape_xml(&self.cot_type);
let callsign = escape_xml(&self.callsign);
let link_uid = escape_xml(&self.link_uid);
let (vehicle_type_tag, video_url) =
match self.vehicle_type_tag.split_once("|armatak_video_url=") {
Some((vehicle_type_tag, video_url)) => (
escape_xml(vehicle_type_tag),
Some(escape_xml(video_url.trim())).filter(|value| !value.is_empty()),
),
None => (escape_xml(&self.vehicle_type_tag), None),
};
let now = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
let stale = (Utc::now() + Duration::milliseconds(3500))
.to_rfc3339_opts(SecondsFormat::Millis, true);
let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
xml.push_str(&format!(
"<event version=\"2.0\" uid=\"{uid}\" type=\"{cot_type}\" time=\"{now}\" start=\"{now}\" stale=\"{stale}\" how=\"m-g\" access=\"Undefined\">",
cot_type = cot_type,
uid = uid,
now = now,
stale = stale,
));
xml.push_str(&format!(
"<point lat=\"{lat}\" lon=\"{lon}\" hae=\"{hae}\" ce=\"9999999.0\" le=\"9999999.0\"/>",
lat = self.point_lat,
lon = self.point_lon,
hae = self.point_hae,
));
xml.push_str("<detail>");
xml.push_str("<_uastool extendedCot=\"true\" activeRoute=\"false\"/>");
xml.push_str(&format!(
"<track course=\"{}\" slope=\"0.0\" speed=\"{}\"/>",
self.track_course, self.track_speed,
));
xml.push_str(&format!(
"<sensor elevation=\"{}\" vfov=\"{}\" north=\"0.0\" roll=\"0.0\" range=\"{}\" azimuth=\"{}\" fov=\"{}\" type=\"r-e\" version=\"0.6\"/>",
self.sensor_elevation,
self.sensor_vfov,
self.sensor_range,
self.sensor_azimuth,
self.sensor_fov,
));
xml.push_str(&format!(
"<spatial><attitude roll=\"{}\" pitch=\"{}\" yaw=\"{}\"/><spin roll=\"0.0\" pitch=\"0.0\" yaw=\"0.0\"/></spatial>",
self.attitude_roll,
self.attitude_pitch,
self.attitude_yaw,
));
xml.push_str(&format!(
"<vehicle goHomeBatteryPercent=\"-2147483648\" hal=\"{}\" flightTimeRemaining=\"-2147483648\" typeTag=\"{}\" batteryRemainingCapacity=\"-2147483648\" isFlying=\"{}\" flightTime=\"-2147483648\" type=\"Generic\" batteryMaxCapacity=\"-2147483648\"/>",
self.hal,
vehicle_type_tag,
if self.is_flying != 0 { "true" } else { "false" },
));
xml.push_str("<_radio rssi=\"-2147483648\" gps=\"false\"/>");
xml.push_str(&format!("<contact callsign=\"{}\"/>", callsign));
xml.push_str("<waypointCollection></waypointCollection>");
xml.push_str(&format!("<_route sender=\"{}\"/>", link_uid));
xml.push_str("<commandedData climbRate=\"0.0\"/>");
if let Some(video_url) = video_url {
xml.push_str(&video_detail_xml(&video_url, &self.uid, &self.callsign));
} else {
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("</detail></event>");
xml
}
}
pub struct UasVideoCoTPayload { pub struct UasVideoCoTPayload {
/// The drone's persistent ATAK UUID (same uid used for PPLI / marker CoT).
pub uid: String, pub uid: String,
/// Human-readable label shown in the UAS Tool video list.
pub callsign: String, pub callsign: String,
/// Full RTSP URL, e.g. "rtsp://192.168.1.10:8554/live/drone1".
pub video_url: String, pub video_url: String,
} }
impl FromArma for UasVideoCoTPayload { impl FromArma for UasVideoCoTPayload {
fn from_arma(data: String) -> Result<UasVideoCoTPayload, FromArmaError> { fn from_arma(data: String) -> Result<UasVideoCoTPayload, FromArmaError> {
let (uid, callsign, video_url) = let (uid, callsign, video_url) = <(String, String, String)>::from_arma(data)?;
<(String, String, String)>::from_arma(data)?;
Ok(Self { Ok(Self {
uid, uid,
callsign, callsign,
@@ -63,8 +212,6 @@ impl FromArma for UasVideoCoTPayload {
} }
impl UasVideoCoTPayload { impl UasVideoCoTPayload {
/// Build the complete XML string for the b-i-v CoT event.
/// Returns an empty string if the RTSP URL cannot be parsed.
pub fn to_xml(&self) -> String { pub fn to_xml(&self) -> String {
let (address, port, path) = match parse_rtsp_url(&self.video_url) { let (address, port, path) = match parse_rtsp_url(&self.video_url) {
Some(parts) => parts, Some(parts) => parts,
@@ -76,103 +223,60 @@ impl UasVideoCoTPayload {
return String::new(); return String::new();
} }
}; };
let callsign = escape_xml(&self.callsign);
let uid = escape_xml(&self.uid);
let address = escape_xml(&address);
let path = escape_xml(&path);
let now = let now = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true); let stale =
// Long stale time: the video endpoint is considered valid for 1 hour. (Utc::now() + Duration::seconds(3600)).to_rfc3339_opts(SecondsFormat::Millis, true);
// The CoT is re-sent every router tick so it stays fresh even if the
// TAK server restarts.
let stale = (Utc::now() + Duration::seconds(3600))
.to_rfc3339_opts(SecondsFormat::Millis, true);
let mut xml = String::new(); let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"); xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
xml.push_str(&format!( xml.push_str(&format!(
"<event type=\"b-i-v\" version=\"2.0\" how=\"m-g\" \ "<event type=\"b-i-v\" version=\"2.0\" how=\"m-g\" uid=\"{uid}\" time=\"{now}\" start=\"{now}\" stale=\"{stale}\">",
uid=\"{uid}\" time=\"{now}\" start=\"{now}\" stale=\"{stale}\">", uid = uid,
uid = self.uid,
now = now, now = now,
stale = stale stale = stale
)); ));
// b-i-v events carry no real geographic position.
xml.push_str( xml.push_str(
"<point lat=\"0\" lon=\"0\" hae=\"9999999.0\" \ "<point lat=\"0\" lon=\"0\" hae=\"9999999.0\" ce=\"9999999.0\" le=\"9999999.0\"/>",
ce=\"9999999.0\" le=\"9999999.0\"/>",
); );
xml.push_str("<detail>"); xml.push_str("<detail>");
xml.push_str("<__video>"); xml.push_str("<__video>");
xml.push_str(&format!( xml.push_str(&format!(
"<ConnectionEntry \ "<ConnectionEntry protocol=\"rtsp\" path=\"{path}\" address=\"{address}\" port=\"{port}\" uid=\"{uid}\" alias=\"{callsign}\" roverPort=\"-1\" rtspReliable=\"0\" ignoreEmbeddedKLV=\"False\" networkTimeout=\"0\" bufferTime=\"-1\"/>",
protocol=\"rtsp\" \
path=\"{path}\" \
address=\"{address}\" \
port=\"{port}\" \
uid=\"{uid}\" \
alias=\"{callsign}\" \
roverPort=\"-1\" \
rtspReliable=\"0\" \
ignoreEmbeddedKLV=\"False\" \
networkTimeout=\"0\" \
bufferTime=\"-1\"/>",
path = path, path = path,
address = address, address = address,
port = port, port = port,
uid = self.uid, uid = uid,
callsign = self.callsign, callsign = callsign,
)); ));
xml.push_str("</__video>"); xml.push_str("</__video>");
xml.push_str(&format!( xml.push_str(&format!("<contact callsign=\"{}\"/>", callsign));
"<contact callsign=\"{}\"/>",
self.callsign
));
xml.push_str("</detail>"); xml.push_str("</detail>");
xml.push_str("</event>"); xml.push_str("</event>");
xml xml
} }
} }
// ---------------------------------------------------------------------------
// b-m-p-s-p-loc Sensor position (FOV cone + video link)
// ---------------------------------------------------------------------------
pub struct UasSensorCoTPayload { pub struct UasSensorCoTPayload {
/// UID for this sensor event — conventionally "<drone-uuid>-sensor".
pub uid: String, pub uid: String,
/// The drone's ATAK UUID; must match the uid used in the b-i-v event so
/// the UAS Tool can link sensor data to the correct video stream.
pub video_uid: String, pub video_uid: String,
/// Callsign shown in the UAS Tool sensor list.
pub callsign: String, pub callsign: String,
/// Drone latitude in decimal degrees (WGS-84).
pub point_lat: f64, pub point_lat: f64,
/// Drone longitude in decimal degrees (WGS-84).
pub point_lon: f64, pub point_lon: f64,
/// Drone height above ellipsoid in metres (WGS-84).
pub point_hae: f32, pub point_hae: f32,
/// Camera azimuth in degrees, clockwise from true North (0359).
pub azimuth: i32, pub azimuth: i32,
/// Camera horizontal field of view in degrees.
pub fov: i32, pub fov: i32,
/// Estimated slant range from drone to ground point in metres.
/// A good approximation is the drone's AGL altitude.
pub range: i32, pub range: i32,
} }
impl FromArma for UasSensorCoTPayload { impl FromArma for UasSensorCoTPayload {
fn from_arma(data: String) -> Result<UasSensorCoTPayload, FromArmaError> { fn from_arma(data: String) -> Result<UasSensorCoTPayload, FromArmaError> {
let ( let (uid, video_uid, callsign, point_lat, point_lon, point_hae, azimuth, fov, range) =
uid, <(String, String, String, f64, f64, f32, i32, i32, i32)>::from_arma(data)?;
video_uid,
callsign,
point_lat,
point_lon,
point_hae,
azimuth,
fov,
range,
) = <(String, String, String, f64, f64, f32, i32, i32, i32)>::from_arma(
data,
)?;
Ok(Self { Ok(Self {
uid, uid,
video_uid, video_uid,
@@ -188,54 +292,37 @@ impl FromArma for UasSensorCoTPayload {
} }
impl UasSensorCoTPayload { impl UasSensorCoTPayload {
/// Build the complete XML string for the b-m-p-s-p-loc CoT event.
pub fn to_xml(&self) -> String { pub fn to_xml(&self) -> String {
let now = let uid = escape_xml(&self.uid);
Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true); let video_uid = escape_xml(&self.video_uid);
// 60-second stale: must be refreshed every router tick (1 s) to keep let callsign = escape_xml(&self.callsign);
// the FOV cone visible on the map. let now = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
let stale = (Utc::now() + Duration::seconds(60)) let stale =
.to_rfc3339_opts(SecondsFormat::Millis, true); (Utc::now() + Duration::seconds(60)).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\" ?>"); xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
xml.push_str(&format!( xml.push_str(&format!(
"<event type=\"b-m-p-s-p-loc\" version=\"2.0\" how=\"h-g-i-g-o\" \ "<event type=\"b-m-p-s-p-loc\" version=\"2.0\" how=\"h-g-i-g-o\" uid=\"{uid}\" time=\"{now}\" start=\"{now}\" stale=\"{stale}\">",
uid=\"{uid}\" time=\"{now}\" start=\"{now}\" stale=\"{stale}\">", uid = uid,
uid = self.uid,
now = now, now = now,
stale = stale, stale = stale,
)); ));
xml.push_str(&format!( xml.push_str(&format!(
"<point lat=\"{lat}\" lon=\"{lon}\" hae=\"{hae}\" \ "<point lat=\"{lat}\" lon=\"{lon}\" hae=\"{hae}\" ce=\"9999999.0\" le=\"9999999.0\"/>",
ce=\"9999999.0\" le=\"9999999.0\"/>",
lat = self.point_lat, lat = self.point_lat,
lon = self.point_lon, lon = self.point_lon,
hae = self.point_hae, hae = self.point_hae,
)); ));
xml.push_str("<detail>"); xml.push_str("<detail>");
// fovAlpha controls the transparency of the FOV cone fill (01).
// 0.537 ≈ 137/255, the value used by the real UAS Tool.
xml.push_str(&format!( xml.push_str(&format!(
"<sensor \ "<sensor fov=\"{fov}\" fovRed=\"1\" fovGreen=\"1\" fovBlue=\"1\" fovAlpha=\"0.5372549\" displayMagneticReference=\"0\" range=\"{range}\" azimuth=\"{az}\"/>",
fov=\"{fov}\" \
fovRed=\"1\" \
fovGreen=\"1\" \
fovBlue=\"1\" \
fovAlpha=\"0.5372549\" \
displayMagneticReference=\"0\" \
range=\"{range}\" \
azimuth=\"{az}\"/>",
fov = self.fov, fov = self.fov,
range = self.range, range = self.range,
az = self.azimuth, az = self.azimuth,
)); ));
// Link this sensor event to the b-i-v video endpoint. xml.push_str(&format!("<__video uid=\"{}\"/>", video_uid));
xml.push_str(&format!("<__video uid=\"{}\"/>", self.video_uid)); xml.push_str(&format!("<contact callsign=\"{}\"/>", callsign));
xml.push_str(&format!(
"<contact callsign=\"{}\"/>",
self.callsign
));
xml.push_str("</detail>"); xml.push_str("</detail>");
xml.push_str("</event>"); xml.push_str("</event>");
xml xml

52
src/cot/video.rs Normal file
View File

@@ -0,0 +1,52 @@
fn escape_xml_attribute(value: &str) -> String {
value
.replace('&', "&amp;")
.replace('"', "&quot;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('\'', "&apos;")
}
fn parse_video_url(url: &str) -> Option<(String, String, String, String)> {
let (protocol, rest) = url.trim().split_once("://")?;
let (authority, path) = match rest.split_once('/') {
Some((authority, path)) => (authority, format!("/{}", path)),
None => (rest, String::new()),
};
let host_port = authority
.rsplit_once('@')
.map_or(authority, |(_, host_port)| host_port);
let (address, port) = host_port.rsplit_once(':')?;
if protocol.is_empty() || address.is_empty() || port.is_empty() {
return None;
}
Some((
protocol.to_ascii_lowercase(),
address.to_string(),
port.to_string(),
path,
))
}
pub fn video_detail_xml(video_url: &str, uid: &str, callsign: &str) -> String {
let trimmed_url = video_url.trim();
if trimmed_url.is_empty() {
return "<__video></__video>".to_string();
}
let Some((protocol, address, port, path)) = parse_video_url(trimmed_url) else {
return format!("<__video url=\"{}\"/>", escape_xml_attribute(trimmed_url));
};
format!(
"<__video><ConnectionEntry protocol=\"{}\" path=\"{}\" address=\"{}\" port=\"{}\" uid=\"{}\" alias=\"{}\" roverPort=\"-1\" rtspReliable=\"0\" ignoreEmbeddedKLV=\"False\" networkTimeout=\"0\" bufferTime=\"-1\"/></__video>",
escape_xml_attribute(&protocol),
escape_xml_attribute(&path),
escape_xml_attribute(&address),
escape_xml_attribute(&port),
escape_xml_attribute(uid),
escape_xml_attribute(callsign),
)
}

View File

@@ -1,8 +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 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;
@@ -39,12 +41,35 @@ pub fn init() -> Extension {
.command("local_ip", utils::address::get_local_address) .command("local_ip", utils::address::get_local_address)
.command("uuid", utils::uuid::get_uuid) .command("uuid", utils::uuid::get_uuid)
.command("log", utils::log::log_info) .command("log", utils::log::log_info)
.group(
"uas",
Group::new()
.command("start_endpoint", uas::start_endpoint)
.command("stop_endpoint", uas::stop_endpoint)
.command("send_uas_telemetry", uas::send_uas_telemetry)
.command("send_uas_system", uas::send_uas_system),
)
.group(
"mdns",
Group::new()
.command("start_uas_advertisement", mdns::start_uas_advertisement)
.command("stop", mdns::stop),
)
.group( .group(
"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(
@@ -60,9 +85,11 @@ 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)
// UAS Tool integration .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)
.command("uas_sensor", tcp::cot::send_uas_sensor_cot), .command("uas_sensor", tcp::cot::send_uas_sensor_cot),
) )
@@ -73,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(

229
src/mdns.rs Normal file
View File

@@ -0,0 +1,229 @@
use arma_rs::Context;
use lazy_static::lazy_static;
use log::info;
use std::net::{Ipv4Addr, SocketAddrV4, UdpSocket};
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::Mutex;
use std::thread;
use std::time::Duration;
lazy_static! {
static ref MDNS_CTRL: Mutex<Option<Sender<()>>> = Mutex::new(None);
}
fn detect_local_ipv4() -> Result<Ipv4Addr, String> {
let socket = UdpSocket::bind("0.0.0.0:0").map_err(|e| e.to_string())?;
socket.connect("8.8.8.8:80").map_err(|e| e.to_string())?;
match socket.local_addr().map_err(|e| e.to_string())? {
std::net::SocketAddr::V4(addr) => Ok(*addr.ip()),
std::net::SocketAddr::V6(_) => Err("Local address is not IPv4".to_string()),
}
}
fn sanitize_label(value: &str, fallback: &str) -> String {
let mut sanitized = value
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' {
c
} else {
'-'
}
})
.collect::<String>()
.trim_matches('-')
.to_string();
if sanitized.is_empty() {
sanitized = fallback.to_string();
}
if sanitized.len() > 63 {
sanitized.truncate(63);
}
sanitized
}
fn encode_name(name: &str) -> Vec<u8> {
let mut encoded = Vec::new();
for label in name.split('.') {
let bytes = label.as_bytes();
encoded.push(bytes.len() as u8);
encoded.extend_from_slice(bytes);
}
encoded.push(0);
encoded
}
fn push_u16(buf: &mut Vec<u8>, value: u16) {
buf.extend_from_slice(&value.to_be_bytes());
}
fn push_u32(buf: &mut Vec<u8>, value: u32) {
buf.extend_from_slice(&value.to_be_bytes());
}
fn push_record(buf: &mut Vec<u8>, name: &str, rr_type: u16, rr_class: u16, ttl: u32, rdata: &[u8]) {
buf.extend_from_slice(&encode_name(name));
push_u16(buf, rr_type);
push_u16(buf, rr_class);
push_u32(buf, ttl);
push_u16(buf, rdata.len() as u16);
buf.extend_from_slice(rdata);
}
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 instance_fqdn = format!("{}.{}", instance_name, service_type);
let host_fqdn = format!("{}.local", host_name);
let mut packet = Vec::new();
push_u16(&mut packet, 0);
push_u16(&mut packet, 0x8400);
push_u16(&mut packet, 0);
push_u16(&mut packet, 4);
push_u16(&mut packet, 0);
push_u16(&mut packet, 0);
let ptr_rdata = encode_name(&instance_fqdn);
push_record(&mut packet, service_type, 12, 0x0001, 120, &ptr_rdata);
let mut srv_rdata = Vec::new();
push_u16(&mut srv_rdata, 0);
push_u16(&mut srv_rdata, 0);
push_u16(&mut srv_rdata, port);
srv_rdata.extend_from_slice(&encode_name(&host_fqdn));
push_record(&mut packet, &instance_fqdn, 33, 0x8001, 120, &srv_rdata);
let txt_value = format!("uri={}", video_uri);
let txt_bytes = txt_value.as_bytes();
let mut txt_rdata = Vec::new();
txt_rdata.push(txt_bytes.len() as u8);
txt_rdata.extend_from_slice(txt_bytes);
push_record(&mut packet, &instance_fqdn, 16, 0x8001, 120, &txt_rdata);
let a_rdata = ip.octets();
push_record(&mut packet, &host_fqdn, 1, 0x8001, 120, &a_rdata);
packet
}
fn stop_existing() {
if let Ok(mut lock) = MDNS_CTRL.lock() {
if let Some(tx) = lock.take() {
let _ = tx.send(());
}
}
}
pub fn start_uas_advertisement(
ctx: Context,
instance_name: String,
mavlink_port: i32,
video_uri: String,
) -> &'static str {
stop_existing();
let local_ip = match detect_local_ipv4() {
Ok(ip) => ip,
Err(error) => {
let _ = ctx.callback_data(
"MDNS ERROR",
"Failed to determine local IPv4",
error.clone(),
);
return "mdns local IPv4 error";
}
};
let port = mavlink_port.clamp(1, 65535) as u16;
let safe_instance = sanitize_label(&instance_name, "ArmaTAK-UAS");
let host_label = sanitize_label(
&format!("armatak-{}", safe_instance.to_lowercase()),
"armatak-uas-host",
);
let packet = build_mdns_packet(&safe_instance, &host_label, local_ip, port, &video_uri);
let callback_video_uri = video_uri.clone();
let multicast_addr = SocketAddrV4::new(Ipv4Addr::new(224, 0, 0, 251), 5353);
let (stop_tx, stop_rx): (Sender<()>, Receiver<()>) = mpsc::channel();
if let Ok(mut lock) = MDNS_CTRL.lock() {
*lock = Some(stop_tx);
}
thread::spawn(move || {
let socket = match UdpSocket::bind("0.0.0.0:0") {
Ok(socket) => socket,
Err(error) => {
info!("mDNS failed to bind UDP socket: {}", error);
return;
}
};
let _ = socket.set_multicast_ttl_v4(255);
let _ = socket.set_multicast_loop_v4(true);
info!(
"Starting mDNS UAS advertisement instance={} host={} ip={} port={} video_uri={}",
safe_instance, host_label, local_ip, port, video_uri
);
loop {
match socket.send_to(&packet, multicast_addr) {
Ok(size) => info!(
"Sent mDNS UAS advertisement ({} bytes) to {}",
size, multicast_addr
),
Err(error) => info!("Failed sending mDNS UAS advertisement: {}", error),
}
match stop_rx.recv_timeout(Duration::from_secs(5)) {
Ok(_) => break,
Err(mpsc::RecvTimeoutError::Timeout) => {}
Err(_) => break,
}
}
info!(
"Stopped mDNS UAS advertisement for instance={}",
safe_instance
);
});
let _ = ctx.callback_data(
"MDNS",
"UAS advertisement started",
format!("{}:{} | {}", local_ip, port, callback_video_uri),
);
"starting mdns uas advertisement"
}
pub fn stop(ctx: Context) -> &'static str {
let had_running = match MDNS_CTRL.lock() {
Ok(mut lock) => {
if let Some(tx) = lock.take() {
let _ = tx.send(());
true
} else {
false
}
}
Err(_) => false,
};
if had_running {
let _ = ctx.callback_null("MDNS", "UAS advertisement stopped");
"stopping mdns advertisement"
} else {
let _ = ctx.callback_null("MDNS ERROR", "No mDNS advertisement is running");
"no mdns advertisement running"
}
}

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,
@@ -40,14 +56,17 @@ pub fn send_message_cot(
"Sending Message CoT to TCP server" "Sending Message CoT to TCP server"
} }
/// Send a b-i-v CoT that declares the RTSP video endpoint for a drone. pub fn send_uas_platform_cot(
/// Called by SQF via: "armatak" callExtension ["tcp_socket:cot:uas_video", [payload]]
///
/// Returns early without sending if the RTSP URL in the payload cannot be parsed.
pub fn send_uas_video_cot(
ctx: Context, ctx: Context,
payload: cot::uas::UasVideoCoTPayload, payload: cot::uas::UasPlatformCoTPayload,
) -> &'static str { ) -> &'static str {
let xml = payload.to_xml();
send_payload(ctx, xml);
"Sending UAS Platform main CoT to TCP server"
}
pub fn send_uas_video_cot(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);
@@ -56,13 +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"
} }
/// Send a b-m-p-s-p-loc CoT carrying the drone camera's azimuth, FOV, and pub fn send_uas_sensor_cot(ctx: Context, payload: cot::uas::UasSensorCoTPayload) -> &'static str {
/// slant-range so the UAS Tool can draw the FOV cone on the map.
/// Called by SQF via: "armatak" callExtension ["tcp_socket:cot:uas_sensor", [payload]]
pub fn send_uas_sensor_cot(
ctx: Context,
payload: cot::uas::UasSensorCoTPayload,
) -> &'static str {
let xml = payload.to_xml(); 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 {:?}",

1021
src/uas/callbacks.rs Normal file

File diff suppressed because it is too large Load Diff

43
src/uas/constants.rs Normal file
View File

@@ -0,0 +1,43 @@
pub const AUTOPILOT_COMPONENT_ID: u8 = 1;
pub const CAMERA_COMPONENT_ID: u8 = 100;
pub const TURRET_CAMERA_COMPONENT_ID: u8 = 101;
pub const GIMBAL_COMPONENT_ID: u8 = 154;
pub const MAV_TYPE_FIXED_WING: u8 = 1;
pub const MAV_TYPE_QUADROTOR: u8 = 2;
pub const MAV_TYPE_HELICOPTER: u8 = 4;
pub const MAV_TYPE_GIMBAL: u8 = 26;
pub const MAV_TYPE_CAMERA: u8 = 30;
pub const MAV_AUTOPILOT_ARDUPILOTMEGA: u8 = 3;
pub const MAV_AUTOPILOT_INVALID: u8 = 8;
pub const MAV_STATE_STANDBY: u8 = 3;
pub const MAV_STATE_ACTIVE: u8 = 4;
pub const MAV_MODE_FLAG_CUSTOM_MODE_ENABLED: u8 = 1;
pub const MAV_MODE_FLAG_SAFETY_ARMED: u8 = 128;
pub const MAV_LANDED_STATE_UNDEFINED: u8 = 0;
pub const MAV_LANDED_STATE_ON_GROUND: u8 = 1;
pub const MAV_LANDED_STATE_IN_AIR: u8 = 2;
pub const MAV_PROTOCOL_CAPABILITY_MISSION_INT: u64 = 4;
pub const MAV_PROTOCOL_CAPABILITY_COMMAND_INT: u64 = 8;
pub const MAV_PROTOCOL_CAPABILITY_FTP: u64 = 32;
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_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 VIDEO_STREAM_STATUS_FLAGS_RUNNING: u16 = 1;
pub const VIDEO_STREAM_TYPE_RTSP: u8 = 0;
pub const VIDEO_STREAM_TYPE_RTPUDP: u8 = 1;
pub const VIDEO_STREAM_TYPE_TCP_MPEG: u8 = 2;
pub const VIDEO_STREAM_TYPE_MPEG_TS: u8 = 3;
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;

102
src/uas/crc.rs Normal file
View File

@@ -0,0 +1,102 @@
use std::sync::atomic::{AtomicU8, Ordering};
static MAVLINK_SEQUENCE: AtomicU8 = AtomicU8::new(0);
#[derive(Clone, Copy)]
pub(crate) struct FieldSpec {
pub ty: &'static str,
pub name: &'static str,
pub array_len: usize,
}
fn crc_accumulate(byte: u8, crc: &mut u16) {
let mut tmp = byte ^ (*crc as u8);
tmp ^= tmp << 4;
*crc = (*crc >> 8) ^ ((tmp as u16) << 8) ^ ((tmp as u16) << 3) ^ ((tmp as u16) >> 4);
}
fn mavlink_crc(bytes: &[u8], crc_extra: u8) -> u16 {
let mut crc = 0xFFFFu16;
for byte in bytes {
crc_accumulate(*byte, &mut crc);
}
crc_accumulate(crc_extra, &mut crc);
crc
}
pub(crate) fn calculate_crc_extra(message_name: &str, base_fields: &[FieldSpec]) -> u8 {
let mut crc = 0xFFFFu16;
for byte in message_name.as_bytes() {
crc_accumulate(*byte, &mut crc);
}
crc_accumulate(b' ', &mut crc);
for field in base_fields {
for byte in field.ty.as_bytes() {
crc_accumulate(*byte, &mut crc);
}
crc_accumulate(b' ', &mut crc);
for byte in field.name.as_bytes() {
crc_accumulate(*byte, &mut crc);
}
crc_accumulate(b' ', &mut crc);
if field.array_len > 0 {
crc_accumulate(field.array_len as u8, &mut crc);
}
}
((crc & 0xFF) ^ (crc >> 8)) as u8
}
pub(crate) fn build_v1_packet(
system_id: u8,
component_id: u8,
msg_id: u8,
payload: &[u8],
crc_extra: u8,
) -> Vec<u8> {
let seq = MAVLINK_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let mut packet = Vec::with_capacity(payload.len() + 8);
packet.push(0xFE);
packet.push(payload.len() as u8);
packet.push(seq);
packet.push(system_id);
packet.push(component_id);
packet.push(msg_id);
packet.extend_from_slice(payload);
let crc = mavlink_crc(&packet[1..], crc_extra);
packet.push((crc & 0xFF) as u8);
packet.push((crc >> 8) as u8);
packet
}
pub(crate) fn build_v2_packet(
system_id: u8,
component_id: u8,
msg_id: u32,
payload: &[u8],
crc_extra: u8,
) -> Vec<u8> {
let seq = MAVLINK_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let mut packet = Vec::with_capacity(payload.len() + 12);
packet.push(0xFD);
packet.push(payload.len() as u8);
packet.push(0);
packet.push(0);
packet.push(seq);
packet.push(system_id);
packet.push(component_id);
packet.push((msg_id & 0xFF) as u8);
packet.push(((msg_id >> 8) & 0xFF) as u8);
packet.push(((msg_id >> 16) & 0xFF) as u8);
packet.extend_from_slice(payload);
let crc = mavlink_crc(&packet[1..], crc_extra);
packet.push((crc & 0xFF) as u8);
packet.push((crc >> 8) as u8);
packet
}

160
src/uas/endpoint.rs Normal file
View File

@@ -0,0 +1,160 @@
use arma_rs::Context;
use log::info;
use std::net::UdpSocket;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::Duration;
use super::callbacks::{mavlink_callback_event, mavlink_packet_summary, mavlink_response_packets};
pub(crate) struct MavlinkEndpoint {
pub socket: UdpSocket,
pub running: Arc<AtomicBool>,
pub listener: Option<JoinHandle<()>>,
pub bind_port: u16,
}
static MAVLINK_ENDPOINT: Mutex<Option<MavlinkEndpoint>> = Mutex::new(None);
pub(crate) fn socket_for_send() -> Option<UdpSocket> {
MAVLINK_ENDPOINT.lock().ok().and_then(|endpoint| {
endpoint
.as_ref()
.and_then(|entry| entry.socket.try_clone().ok())
})
}
fn stop_endpoint_internal() {
let endpoint = MAVLINK_ENDPOINT.lock().unwrap().take();
if let Some(mut endpoint) = endpoint {
endpoint.running.store(false, Ordering::Relaxed);
info!(
"Stopping MAVLink UDP endpoint on 0.0.0.0:{}",
endpoint.bind_port
);
if let Some(listener) = endpoint.listener.take() {
let _ = listener.join();
}
}
}
pub fn start_endpoint(ctx: Context, bind_port: i32) -> &'static str {
let bind_port = bind_port.clamp(1, 65535) as u16;
stop_endpoint_internal();
let socket = match UdpSocket::bind(format!("0.0.0.0:{bind_port}")) {
Ok(socket) => socket,
Err(error) => {
let _ = ctx.callback_data(
"MAVLINK UDP ERROR",
"failed to bind MAVLink UDP endpoint",
error.to_string(),
);
info!(
"Failed to bind MAVLink UDP endpoint on 0.0.0.0:{}: {}",
bind_port, error
);
return "Failed to bind MAVLink UDP endpoint";
}
};
if let Err(error) = socket.set_read_timeout(Some(Duration::from_millis(500))) {
info!(
"Failed to set MAVLink UDP endpoint read timeout on 0.0.0.0:{}: {}",
bind_port, error
);
}
let listener_socket = match socket.try_clone() {
Ok(listener_socket) => listener_socket,
Err(error) => {
let _ = ctx.callback_data(
"MAVLINK UDP ERROR",
"failed to clone MAVLink UDP endpoint socket",
error.to_string(),
);
info!(
"Failed to clone MAVLink UDP endpoint socket on 0.0.0.0:{}: {}",
bind_port, error
);
return "Failed to clone MAVLink UDP endpoint socket";
}
};
let running = Arc::new(AtomicBool::new(true));
let listener_running = Arc::clone(&running);
let listener_ctx = ctx;
let listener = thread::spawn(move || {
let mut buffer = [0u8; 2048];
info!("MAVLink UDP endpoint listening on 0.0.0.0:{}", bind_port);
while listener_running.load(Ordering::Relaxed) {
match listener_socket.recv_from(&mut buffer) {
Ok((received, source)) => {
let source_string = source.to_string();
info!(
"MAVLink UDP endpoint received {} bytes from {}: {}",
received,
source,
mavlink_packet_summary(&buffer[..received])
);
if let Some(event) = mavlink_callback_event(&buffer[..received], &source_string)
{
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)
if matches!(
error.kind(),
std::io::ErrorKind::WouldBlock
| std::io::ErrorKind::TimedOut
| std::io::ErrorKind::ConnectionReset
) => {}
Err(error) => {
if listener_running.load(Ordering::Relaxed) {
info!(
"MAVLink UDP endpoint listener error on 0.0.0.0:{}: {}",
bind_port, error
);
}
break;
}
}
}
info!(
"MAVLink UDP endpoint listener stopped on 0.0.0.0:{}",
bind_port
);
});
*MAVLINK_ENDPOINT.lock().unwrap() = Some(MavlinkEndpoint {
socket,
running,
listener: Some(listener),
bind_port,
});
info!("Started MAVLink UDP endpoint on 0.0.0.0:{}", bind_port);
"Started MAVLink UDP endpoint"
}
pub fn stop_endpoint(_ctx: Context) -> &'static str {
stop_endpoint_internal();
"Stopped MAVLink UDP endpoint"
}

95
src/uas/identity.rs Normal file
View File

@@ -0,0 +1,95 @@
use super::constants::{MAV_TYPE_FIXED_WING, MAV_TYPE_HELICOPTER, MAV_TYPE_QUADROTOR};
pub(crate) fn map_vehicle_type(vehicle_type: u8) -> u8 {
match vehicle_type {
1 => MAV_TYPE_FIXED_WING,
3 => MAV_TYPE_HELICOPTER,
2 => MAV_TYPE_QUADROTOR,
value => value,
}
}
pub(crate) fn normalize_heading_deg(value: f32) -> f32 {
((value % 360.0) + 360.0) % 360.0
}
pub(crate) fn stable_system_id(entity_uuid: &str) -> u8 {
let mut hash: u32 = 0x811C9DC5;
for byte in entity_uuid.as_bytes() {
hash ^= *byte as u32;
hash = hash.wrapping_mul(0x01000193);
}
((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] {
let hex = entity_uuid.replace('-', "");
let mut bytes = [0u8; 16];
for index in 0..16 {
let start = index * 2;
if start + 2 <= hex.len() {
if let Ok(value) = u8::from_str_radix(&hex[start..start + 2], 16) {
bytes[index] = value;
}
}
}
bytes
}
pub(crate) fn uid64_from_uuid(entity_uuid: &str) -> u64 {
let uuid = uuid16(entity_uuid);
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&uuid[..8]);
u64::from_le_bytes(bytes)
}
pub(crate) fn uid2_from_uuid(entity_uuid: &str) -> [u8; 18] {
let uuid = uuid16(entity_uuid);
let mut uid2 = [0u8; 18];
uid2[..16].copy_from_slice(&uuid);
let checksum = entity_uuid
.as_bytes()
.iter()
.fold(0u16, |acc, value| acc.wrapping_add(*value as u16));
uid2[16] = (checksum & 0xFF) as u8;
uid2[17] = (checksum >> 8) as u8;
uid2
}
pub(crate) fn fixed_string<const N: usize>(value: &str) -> [u8; N] {
let mut bytes = [0u8; N];
let raw = value.as_bytes();
let len = raw.len().min(N.saturating_sub(1));
bytes[..len].copy_from_slice(&raw[..len]);
bytes
}
pub(crate) fn should_send_video_stream_information(video_uri: &str) -> bool {
let trimmed = video_uri.trim().to_ascii_lowercase();
trimmed.starts_with("rtsp://")
|| trimmed.starts_with("rtp://")
|| trimmed.starts_with("udp://")
|| trimmed.starts_with("mpegts://")
|| trimmed.starts_with("tcp://")
}

14
src/uas/mod.rs Normal file
View File

@@ -0,0 +1,14 @@
mod callbacks;
mod constants;
mod crc;
mod endpoint;
mod identity;
mod packets;
mod payload;
mod send;
mod state;
pub use endpoint::{start_endpoint, stop_endpoint};
#[allow(unused_imports)]
pub use payload::{UasSystemPayload, UasTelemetryPayload};
pub use send::{send_uas_system, send_uas_telemetry};

644
src/uas/packets.rs Normal file
View File

@@ -0,0 +1,644 @@
use chrono::Utc;
use super::constants::{
AUTOPILOT_COMPONENT_ID, CAMERA_CAP_FLAGS_CAPTURE_IMAGE, CAMERA_CAP_FLAGS_CAPTURE_VIDEO,
CAMERA_CAP_FLAGS_HAS_BASIC_ZOOM, CAMERA_CAP_FLAGS_HAS_MODES, CAMERA_CAP_FLAGS_HAS_VIDEO_STREAM,
GIMBAL_COMPONENT_ID, GIMBAL_MANAGER_CAP_FLAGS_BASIC_PITCH_YAW, MAV_AUTOPILOT_ARDUPILOTMEGA,
MAV_AUTOPILOT_INVALID, MAV_LANDED_STATE_IN_AIR, MAV_LANDED_STATE_ON_GROUND,
MAV_LANDED_STATE_UNDEFINED, MAV_MODE_FLAG_CUSTOM_MODE_ENABLED, MAV_MODE_FLAG_SAFETY_ARMED,
MAV_PROTOCOL_CAPABILITY_COMMAND_INT,
MAV_PROTOCOL_CAPABILITY_COMPONENT_IMPLEMENTS_GIMBAL_MANAGER, MAV_PROTOCOL_CAPABILITY_FTP,
MAV_PROTOCOL_CAPABILITY_MAVLINK2, MAV_PROTOCOL_CAPABILITY_MISSION_INT,
MAV_PROTOCOL_CAPABILITY_SET_POSITION_TARGET_GLOBAL_INT, MAV_STATE_ACTIVE, MAV_STATE_STANDBY,
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::identity::{fixed_string, normalize_heading_deg, uid2_from_uuid, uid64_from_uuid};
use super::payload::UasTelemetryPayload;
pub(crate) fn heartbeat_packet(payload: &UasTelemetryPayload) -> Vec<u8> {
let mut msg = Vec::with_capacity(9);
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(MAV_AUTOPILOT_ARDUPILOTMEGA);
msg.push(if payload.flying {
MAV_MODE_FLAG_CUSTOM_MODE_ENABLED | MAV_MODE_FLAG_SAFETY_ARMED
} else {
MAV_MODE_FLAG_CUSTOM_MODE_ENABLED
});
msg.push(if payload.flying {
MAV_STATE_ACTIVE
} else {
MAV_STATE_STANDBY
});
msg.push(3);
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> {
let time_usec = (Utc::now().timestamp_millis().max(0) as u64) * 1_000;
let fix_type = if payload.flying { 3u8 } else { 2u8 };
let mut msg = Vec::with_capacity(30);
msg.extend_from_slice(&time_usec.to_le_bytes());
msg.extend_from_slice(&((payload.lat_deg * 1e7).round() as i32).to_le_bytes());
msg.extend_from_slice(&((payload.lon_deg * 1e7).round() as i32).to_le_bytes());
msg.extend_from_slice(&((payload.alt_msl_m * 1000.0).round() as i32).to_le_bytes());
msg.extend_from_slice(&u16::MAX.to_le_bytes());
msg.extend_from_slice(&u16::MAX.to_le_bytes());
msg.extend_from_slice(&u16::MAX.to_le_bytes());
msg.extend_from_slice(&u16::MAX.to_le_bytes());
msg.push(fix_type);
msg.push(10);
build_v1_packet(payload.system_id, payload.component_id, 24, &msg, 24)
}
pub(crate) fn global_position_int_packet(payload: &UasTelemetryPayload) -> Vec<u8> {
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 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 mut msg = Vec::with_capacity(28);
msg.extend_from_slice(&time_boot_ms.to_le_bytes());
msg.extend_from_slice(&((payload.lat_deg * 1e7).round() as i32).to_le_bytes());
msg.extend_from_slice(&((payload.lon_deg * 1e7).round() as i32).to_le_bytes());
msg.extend_from_slice(&((payload.alt_msl_m * 1000.0).round() as i32).to_le_bytes());
msg.extend_from_slice(&((payload.rel_alt_m * 1000.0).round() as i32).to_le_bytes());
msg.extend_from_slice(&vx.to_le_bytes());
msg.extend_from_slice(&vy.to_le_bytes());
msg.extend_from_slice(&0i16.to_le_bytes());
msg.extend_from_slice(&heading.to_le_bytes());
build_v1_packet(payload.system_id, payload.component_id, 33, &msg, 104)
}
pub(crate) fn attitude_packet(payload: &UasTelemetryPayload) -> Vec<u8> {
let now_ms = Utc::now().timestamp_millis().max(0) as u32;
let roll = payload.roll_deg.to_radians();
let pitch = payload.pitch_deg.to_radians();
let yaw = payload.yaw_deg.to_radians();
let mut msg = Vec::with_capacity(28);
msg.extend_from_slice(&now_ms.to_le_bytes());
msg.extend_from_slice(&roll.to_le_bytes());
msg.extend_from_slice(&pitch.to_le_bytes());
msg.extend_from_slice(&yaw.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_v1_packet(payload.system_id, payload.component_id, 30, &msg, 39)
}
pub(crate) fn vfr_hud_packet(payload: &UasTelemetryPayload) -> Vec<u8> {
let heading = normalize_heading_deg(payload.heading_deg).round() as i16;
let throttle = if payload.flying { 50u16 } else { 0u16 };
let mut msg = Vec::with_capacity(20);
msg.extend_from_slice(&payload.groundspeed_mps.to_le_bytes());
msg.extend_from_slice(&payload.groundspeed_mps.to_le_bytes());
msg.extend_from_slice(&payload.alt_msl_m.to_le_bytes());
msg.extend_from_slice(&0f32.to_le_bytes());
msg.extend_from_slice(&heading.to_le_bytes());
msg.extend_from_slice(&throttle.to_le_bytes());
build_v1_packet(payload.system_id, payload.component_id, 74, &msg, 20)
}
pub(crate) fn system_status_packet(system_id: u8, battery_remaining_pct: i8) -> Vec<u8> {
let fields = [
FieldSpec {
ty: "uint32_t",
name: "onboard_control_sensors_present",
array_len: 0,
},
FieldSpec {
ty: "uint32_t",
name: "onboard_control_sensors_enabled",
array_len: 0,
},
FieldSpec {
ty: "uint32_t",
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 sensors = 0x2000u32 | 0x4000u32 | 0x8000u32 | 0x20u32;
let mut msg = Vec::with_capacity(31);
msg.extend_from_slice(&sensors.to_le_bytes());
msg.extend_from_slice(&sensors.to_le_bytes());
msg.extend_from_slice(&sensors.to_le_bytes());
msg.extend_from_slice(&500u16.to_le_bytes());
msg.extend_from_slice(&12000u16.to_le_bytes());
msg.extend_from_slice(&(-1i16).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.extend_from_slice(&0u16.to_le_bytes());
msg.push(battery_remaining_pct.clamp(0, 100) as u8);
build_v2_packet(system_id, AUTOPILOT_COMPONENT_ID, 1, &msg, crc_extra)
}
pub(crate) fn extended_sys_state_packet(system_id: u8, landed: bool) -> Vec<u8> {
let fields = [
FieldSpec {
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 mut msg = Vec::with_capacity(2);
msg.push(MAV_LANDED_STATE_UNDEFINED);
msg.push(if landed {
MAV_LANDED_STATE_ON_GROUND
} else {
MAV_LANDED_STATE_IN_AIR
});
build_v2_packet(system_id, AUTOPILOT_COMPONENT_ID, 245, &msg, crc_extra)
}
pub(crate) fn autopilot_version_packet(system_id: u8, entity_uuid: &str) -> Vec<u8> {
let fields = [
FieldSpec {
ty: "uint64_t",
name: "capabilities",
array_len: 0,
},
FieldSpec {
ty: "uint32_t",
name: "flight_sw_version",
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 capabilities = MAV_PROTOCOL_CAPABILITY_MISSION_INT
| MAV_PROTOCOL_CAPABILITY_COMMAND_INT
| MAV_PROTOCOL_CAPABILITY_FTP
| MAV_PROTOCOL_CAPABILITY_SET_POSITION_TARGET_GLOBAL_INT
| MAV_PROTOCOL_CAPABILITY_MAVLINK2
| MAV_PROTOCOL_CAPABILITY_COMPONENT_IMPLEMENTS_GIMBAL_MANAGER;
let uid = uid64_from_uuid(entity_uuid);
let uid2 = uid2_from_uuid(entity_uuid);
let mut msg = Vec::with_capacity(78);
msg.extend_from_slice(&capabilities.to_le_bytes());
msg.extend_from_slice(&0x010100FFu32.to_le_bytes());
msg.extend_from_slice(&0u32.to_le_bytes());
msg.extend_from_slice(&0u32.to_le_bytes());
msg.extend_from_slice(&0u32.to_le_bytes());
msg.extend_from_slice(&[0u8; 8]);
msg.extend_from_slice(&[0u8; 8]);
msg.extend_from_slice(&[0u8; 8]);
msg.extend_from_slice(&0x5441u16.to_le_bytes());
msg.extend_from_slice(&0x5541u16.to_le_bytes());
msg.extend_from_slice(&uid.to_le_bytes());
msg.extend_from_slice(&uid2);
build_v2_packet(system_id, AUTOPILOT_COMPONENT_ID, 148, &msg, crc_extra)
}
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);
msg.extend_from_slice(&0u32.to_le_bytes());
msg.push(component_type);
msg.push(MAV_AUTOPILOT_INVALID);
msg.push(0);
msg.push(MAV_STATE_ACTIVE);
msg.push(3);
build_v2_packet(system_id, component_id, 0, &msg, 50)
}
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 model = fixed_string::<32>(callsign);
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);
msg.extend_from_slice(&(Utc::now().timestamp_millis().max(0) as u32).to_le_bytes());
msg.extend_from_slice(&0x010100FFu32.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(&4.8f32.to_le_bytes());
msg.extend_from_slice(&flags.to_le_bytes());
msg.extend_from_slice(&1280u16.to_le_bytes());
msg.extend_from_slice(&720u16.to_le_bytes());
msg.extend_from_slice(&0u16.to_le_bytes());
msg.extend_from_slice(&vendor);
msg.extend_from_slice(&model);
msg.push(0);
msg.extend_from_slice(&cam_definition_uri);
msg.push(gimbal_device_id);
msg.push(0);
build_v2_packet(system_id, component_id, 259, &msg, 92)
}
pub(crate) fn video_stream_information_packet_for_component(
system_id: u8,
component_id: u8,
callsign: &str,
video_uri: &str,
hfov_deg: f32,
stream_id: u8,
stream_count: u8,
thermal: bool,
) -> Vec<u8> {
let stream_type = if video_uri.starts_with("rtsp://") {
VIDEO_STREAM_TYPE_RTSP
} else if video_uri.starts_with("rtp://") {
VIDEO_STREAM_TYPE_RTPUDP
} else if video_uri.starts_with("tcp://") {
VIDEO_STREAM_TYPE_TCP_MPEG
} else if video_uri.starts_with("mpegts://") || video_uri.starts_with("udp://") {
VIDEO_STREAM_TYPE_MPEG_TS
} else {
VIDEO_STREAM_TYPE_RTSP
};
let name = fixed_string::<32>(callsign);
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);
msg.extend_from_slice(&30f32.to_le_bytes());
msg.extend_from_slice(&4_000_000u32.to_le_bytes());
msg.extend_from_slice(&flags.to_le_bytes());
msg.extend_from_slice(&1280u16.to_le_bytes());
msg.extend_from_slice(&720u16.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.push(stream_id);
msg.push(stream_count);
msg.push(stream_type);
msg.extend_from_slice(&name);
msg.extend_from_slice(&uri);
msg.push(VIDEO_STREAM_ENCODING_H264);
msg.push(0);
build_v2_packet(system_id, component_id, 269, &msg, 109)
}
pub(crate) fn video_stream_status_packet_for_component(
system_id: u8,
component_id: u8,
hfov_deg: f32,
stream_id: u8,
thermal: bool,
) -> Vec<u8> {
let flags = VIDEO_STREAM_STATUS_FLAGS_RUNNING | if thermal { 2 } else { 0 };
let mut msg = Vec::with_capacity(19);
msg.extend_from_slice(&30f32.to_le_bytes());
msg.extend_from_slice(&4_000_000u32.to_le_bytes());
msg.extend_from_slice(&flags.to_le_bytes());
msg.extend_from_slice(&1280u16.to_le_bytes());
msg.extend_from_slice(&720u16.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.push(stream_id);
msg.push(0);
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,
]
}

190
src/uas/payload.rs Normal file
View File

@@ -0,0 +1,190 @@
use arma_rs::{FromArma, FromArmaError};
pub struct UasTelemetryPayload {
pub address: String,
pub system_id: u8,
pub component_id: u8,
pub vehicle_type: u8,
pub lat_deg: f64,
pub lon_deg: f64,
pub alt_msl_m: f32,
pub rel_alt_m: f32,
pub heading_deg: f32,
pub groundspeed_mps: f32,
pub roll_deg: f32,
pub pitch_deg: f32,
pub yaw_deg: f32,
pub flying: bool,
}
#[allow(dead_code)]
pub struct UasSystemPayload {
pub address: String,
pub entity_uuid: String,
pub callsign: String,
pub vehicle_type: u8,
pub lat_deg: f64,
pub lon_deg: f64,
pub alt_msl_m: f32,
pub rel_alt_m: f32,
pub heading_deg: f32,
pub groundspeed_mps: f32,
pub roll_deg: f32,
pub pitch_deg: f32,
pub yaw_deg: f32,
pub flying: bool,
pub landed: bool,
pub gimbal_roll_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 battery_remaining_pct: i8,
}
impl FromArma for UasTelemetryPayload {
fn from_arma(data: String) -> Result<Self, FromArmaError> {
let (
address,
system_id,
component_id,
vehicle_type,
lat_deg,
lon_deg,
alt_msl_m,
rel_alt_m,
heading_deg,
groundspeed_mps,
roll_deg,
pitch_deg,
yaw_deg,
flying,
) = <(
String,
i32,
i32,
i32,
f64,
f64,
f32,
f32,
f32,
f32,
f32,
f32,
f32,
i32,
)>::from_arma(data)?;
Ok(Self {
address,
system_id: system_id.clamp(1, 255) as u8,
component_id: component_id.clamp(1, 255) as u8,
vehicle_type: vehicle_type.clamp(0, 255) as u8,
lat_deg,
lon_deg,
alt_msl_m,
rel_alt_m,
heading_deg,
groundspeed_mps,
roll_deg,
pitch_deg,
yaw_deg,
flying: flying != 0,
})
}
}
impl FromArma for UasSystemPayload {
fn from_arma(data: String) -> Result<Self, FromArmaError> {
let (
address,
entity_uuid,
callsign,
vehicle_type,
lat_deg,
lon_deg,
alt_msl_m,
rel_alt_m,
heading_deg,
groundspeed_mps,
roll_deg,
pitch_deg,
yaw_deg,
flying,
landed,
gimbal_roll_deg,
gimbal_pitch_deg,
gimbal_yaw_deg,
video_uri,
hfov_deg,
vfov_deg,
image_lat_deg,
image_lon_deg,
image_alt_msl_m,
has_turret_camera,
battery_remaining_pct,
) = <(
String,
String,
String,
i32,
f64,
f64,
f32,
f32,
f32,
f32,
f32,
f32,
f32,
i32,
i32,
f32,
f32,
f32,
String,
f32,
f32,
f64,
f64,
f32,
i32,
i32,
)>::from_arma(data)?;
Ok(Self {
address,
entity_uuid,
callsign,
vehicle_type: vehicle_type.clamp(0, 255) as u8,
lat_deg,
lon_deg,
alt_msl_m,
rel_alt_m,
heading_deg,
groundspeed_mps,
roll_deg,
pitch_deg,
yaw_deg,
flying: flying != 0,
landed: landed != 0,
gimbal_roll_deg,
gimbal_pitch_deg,
gimbal_yaw_deg,
video_uri,
hfov_deg,
vfov_deg,
image_lat_deg,
image_lon_deg,
image_alt_msl_m,
has_turret_camera: has_turret_camera != 0,
battery_remaining_pct: battery_remaining_pct.clamp(0, 100) as i8,
})
}
}

403
src/uas/send.rs Normal file
View File

@@ -0,0 +1,403 @@
use arma_rs::Context;
use log::info;
use std::net::UdpSocket;
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::identity::{
map_vehicle_type, should_send_video_stream_information, stable_mavlink_identity,
stable_system_id,
};
use super::packets::{
attitude_packet, autopilot_version_packet, camera_fov_status_packet_for_component,
camera_information_packet_for_component, component_heartbeat_packet, extended_sys_state_packet,
gimbal_manager_information_packet, global_position_int_packet, gps_raw_int_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::state::{latest_system, record_system};
fn sending_socket(ctx: &Context, error_prefix: &str) -> Result<UdpSocket, &'static str> {
if let Some(socket) = socket_for_send() {
return Ok(socket);
}
match UdpSocket::bind("0.0.0.0:0") {
Ok(socket) => Ok(socket),
Err(error) => {
let _ = ctx.callback_data(
"MAVLINK MOCK ERROR",
"Failed to bind UDP socket",
error.to_string(),
);
info!("{} failed to bind UDP socket: {}", error_prefix, error);
Err("Failed to bind MAVLink mock socket")
}
}
}
pub fn send_uas_telemetry(ctx: Context, payload: UasTelemetryPayload) -> &'static str {
info!(
"MAVLink mock send requested to {} sysid={} compid={} lat={} lon={} alt_msl={} rel_alt={} heading={} speed={} flying={}",
payload.address,
payload.system_id,
payload.component_id,
payload.lat_deg,
payload.lon_deg,
payload.alt_msl_m,
payload.rel_alt_m,
payload.heading_deg,
payload.groundspeed_mps,
payload.flying
);
let socket = match sending_socket(&ctx, "MAVLink mock") {
Ok(socket) => socket,
Err(message) => return message,
};
let packets = [
heartbeat_packet(&payload),
gps_raw_int_packet(&payload),
global_position_int_packet(&payload),
attitude_packet(&payload),
vfr_hud_packet(&payload),
];
for (index, packet) in packets.iter().enumerate() {
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(),
);
info!(
"MAVLink mock failed sending packet {} to {}: {}",
index, payload.address, error
);
return "Failed to send MAVLink mock telemetry";
}
}
info!(
"MAVLink mock sent {} packets to {}",
packets.len(),
payload.address
);
"Sent MAVLink mock telemetry"
}
pub fn send_uas_system(ctx: Context, payload: UasSystemPayload) -> &'static str {
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);
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!(
"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.entity_uuid,
mavlink_identity,
system_id,
payload.callsign,
payload.lat_deg,
payload.lon_deg,
payload.alt_msl_m,
payload.rel_alt_m,
payload.heading_deg,
payload.gimbal_pitch_deg,
payload.gimbal_yaw_deg,
payload.video_uri
);
let socket = match sending_socket(&ctx, "MAVLink system") {
Ok(socket) => socket,
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 {
address: payload.address.clone(),
system_id,
component_id: AUTOPILOT_COMPONENT_ID,
vehicle_type,
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,
groundspeed_mps: payload.groundspeed_mps,
roll_deg: payload.roll_deg,
pitch_deg: payload.pitch_deg,
yaw_deg: payload.yaw_deg,
flying: payload.flying,
};
let mut packets = vec![
heartbeat_packet(&autopilot_payload),
gps_raw_int_packet(&autopilot_payload),
global_position_int_packet(&autopilot_payload),
attitude_packet(&autopilot_payload),
vfr_hud_packet(&autopilot_payload),
system_status_packet(system_id, payload.battery_remaining_pct),
extended_sys_state_packet(system_id, payload.landed),
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, GIMBAL_COMPONENT_ID, MAV_TYPE_GIMBAL),
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) {
info!(
"Sending VIDEO_STREAM_INFORMATION for sysid={} uri={}",
system_id, payload.video_uri
);
packets.push(video_stream_information_packet_for_component(
system_id,
CAMERA_COMPONENT_ID,
&format!("{} FPV", payload.callsign),
&payload.video_uri,
payload.hfov_deg,
1,
1,
false,
));
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() {
info!(
"Skipping VIDEO_STREAM_INFORMATION for sysid={} because URI is not a supported stream URI: {}",
system_id, payload.video_uri
);
}
for (index, packet) in packets.iter().enumerate() {
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(),
);
info!(
"MAVLink system failed sending packet {} to {}: {}",
index, payload.address, error
);
return "Failed to send MAVLink system telemetry";
}
}
info!(
"MAVLink system sent {} packets to {} for sysid={} (camera comp={}, gimbal comp={})",
packets.len(),
payload.address,
system_id,
CAMERA_COMPONENT_ID,
GIMBAL_COMPONENT_ID
);
"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

@@ -15,15 +15,14 @@ pub enum UdpCommand {
pub struct UdpClient { pub struct UdpClient {
pub(crate) tx: Sender<UdpCommand>, pub(crate) tx: Sender<UdpCommand>,
pub(crate) address: String,
} }
impl UdpClient { impl UdpClient {
pub fn start(&self, address: String, rx: Receiver<UdpCommand>, ctx: Context) { pub fn start(&self, address: String, rx: Receiver<UdpCommand>, ctx: Context) {
if let Some(ref client) = *UDP_CLIENT.lock().unwrap() {
client.stop();
}
thread::spawn(move || { thread::spawn(move || {
info!("Starting UDP client thread for destination {}", address);
let socket = match UdpSocket::bind("0.0.0.0:0") { let socket = match UdpSocket::bind("0.0.0.0:0") {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
@@ -32,19 +31,28 @@ impl UdpClient {
"Failed to bind UDP socket", "Failed to bind UDP socket",
e.to_string(), e.to_string(),
); );
info!("Failed to bind UDP socket: {}", e); info!("Failed to bind UDP socket for {}: {}", address, e);
return; return;
} }
}; };
if let Ok(local_addr) = socket.local_addr() {
info!(
"UDP client bound local socket {} for destination {}",
local_addr, address
);
}
let _ = ctx.callback_data("UDP SOCKET", "EUD Connected", address.clone()); let _ = ctx.callback_data("UDP SOCKET", "EUD Connected", address.clone());
info!("UDP client reported EUD Connected for {}", address);
let mut running = true; let mut running = true;
while running { while running {
match rx.recv() { match rx.recv() {
Ok(UdpCommand::SendMessage(message, context)) => { Ok(UdpCommand::SendMessage(message, context)) => {
info!("UDP client sending {} bytes to {}", message.len(), address);
if let Err(e) = socket.send_to(message.as_bytes(), &address) { if let Err(e) = socket.send_to(message.as_bytes(), &address) {
info!("Failed to send UDP message: {}", e); info!("Failed to send UDP message to {}: {}", address, e);
let _ = context.callback_data( let _ = context.callback_data(
"UDP SOCKET ERROR", "UDP SOCKET ERROR",
"Failed to send UDP message", "Failed to send UDP message",
@@ -54,13 +62,15 @@ impl UdpClient {
} }
Ok(UdpCommand::Stop) => { Ok(UdpCommand::Stop) => {
running = false; running = false;
info!("Stopping UDP client."); info!("Stopping UDP client for {}", address);
} }
Err(error) => { Err(error) => {
info!("Error receiving command: {}", error.to_string()); info!("Error receiving UDP command for {}: {}", address, error);
} }
} }
} }
info!("UDP client thread exited for {}", address);
}); });
} }
@@ -73,7 +83,9 @@ impl UdpClient {
pub fn stop(&self) { pub fn stop(&self) {
let tx = self.tx.clone(); let tx = self.tx.clone();
let address = self.address.clone();
thread::spawn(move || { thread::spawn(move || {
info!("Queueing stop for UDP client {}", address);
tx.send(UdpCommand::Stop).unwrap(); tx.send(UdpCommand::Stop).unwrap();
}); });
} }
@@ -81,26 +93,56 @@ 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 {
info!("UDP socket start requested for {}", address);
let (tx, rx): (Sender<UdpCommand>, Receiver<UdpCommand>) = mpsc::channel(); let (tx, rx): (Sender<UdpCommand>, Receiver<UdpCommand>) = mpsc::channel();
let client = UdpClient { tx }; let client = UdpClient {
client.start(address, rx, ctx); tx,
address: address.clone(),
};
{
let mut client_guard = UDP_CLIENT.lock().unwrap(); let mut client_guard = UDP_CLIENT.lock().unwrap();
*client_guard = Some(client); if let Some(ref existing_client) = *client_guard {
info!(
"Stopping previous 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 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 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"
} }
@@ -115,12 +157,134 @@ 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);
client.stop(); client.stop();
let _ = ctx.callback_null("UDP SOCKET", "EUD Disconnected"); let _ = ctx.callback_null("UDP SOCKET", "EUD Disconnected");
} else { } else {
let _ = ctx.callback_null("UDP SOCKET ERROR", "UDP Socket is not running"); let _ = ctx.callback_null("UDP SOCKET ERROR", "UDP Socket is 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

@@ -1,5 +1,6 @@
use arma_rs::Context; use arma_rs::Context;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use log::info;
use std::process::Command; use std::process::Command;
use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::Mutex; use std::sync::Mutex;
@@ -16,53 +17,34 @@ lazy_static! {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000; const CREATE_NO_WINDOW: u32 = 0x08000000;
fn build_rtsp_url( fn stop_existing_stream() {
address: &str, if let Ok(mut lock) = STREAM_CTRL.lock() {
port: &str, if let Some(tx) = lock.take() {
stream_path: &str, let _ = tx.send(());
username: &str, }
password: &str,
) -> String {
if username.is_empty() || password.is_empty() {
format!("rtsp://{}:{}/{}", address, port, stream_path)
} else {
format!(
"rtsp://{}:{}@{}:{}/{}",
username, password, address, port, stream_path
)
} }
} }
#[cfg(any(target_os = "windows", target_os = "linux"))] fn spawn_ffmpeg_with_args(
fn spawn_ffmpeg(rtsp_url: String, stop_rx: Receiver<()>, status_tx: Sender<Result<(), String>>) { mut cmd: Command,
stop_rx: Receiver<()>,
status_tx: Sender<Result<(), String>>,
description: String,
) {
thread::spawn(move || { thread::spawn(move || {
let mut cmd = Command::new("ffmpeg"); info!("Starting FFmpeg video stream: {}", description);
cmd.args(&[
"-f",
"x11grab",
"-framerate",
"30",
"-video_size",
"1920x1080",
"-i",
":0",
"-f",
"rtsp",
"-rtsp_transport",
"tcp",
&rtsp_url,
]);
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let child_result = cmd.creation_flags(CREATE_NO_WINDOW).spawn(); let child_result = cmd.creation_flags(CREATE_NO_WINDOW).spawn();
#[cfg(target_os = "linux")] #[cfg(not(target_os = "windows"))]
let child_result = cmd.spawn(); let child_result = cmd.spawn();
match child_result { match child_result {
Ok(mut child) => { Ok(mut child) => {
let _ = status_tx.send(Ok(())); let _ = status_tx.send(Ok(()));
let _ = stop_rx.recv(); let _ = stop_rx.recv();
info!("Stopping FFmpeg video stream: {}", description);
let _ = child.kill(); let _ = child.kill();
} }
Err(e) => { Err(e) => {
@@ -80,30 +62,80 @@ pub fn start_stream(
username: String, username: String,
password: String, password: String,
) -> &'static str { ) -> &'static str {
#[cfg(any(target_os = "windows", target_os = "linux"))] stop_existing_stream();
{
let (stop_tx, stop_rx) = mpsc::channel(); let (stop_tx, stop_rx) = mpsc::channel();
let (status_tx, status_rx) = mpsc::channel(); let (status_tx, status_rx) = mpsc::channel();
let rtsp_url = build_rtsp_url(&address, &port, &stream_path, &username, &password); let rtsp_url = if username.is_empty() || password.is_empty() {
format!("rtsp://{}:{}/{}", address, port, stream_path)
} else {
format!(
"rtsp://{}:{}@{}:{}/{}",
username, password, address, port, stream_path
)
};
spawn_ffmpeg(rtsp_url, stop_rx, status_tx); let mut cmd = Command::new("ffmpeg");
#[cfg(target_os = "windows")]
cmd.args([
"-f",
"gdigrab",
"-framerate",
"15",
"-i",
"desktop",
"-an",
"-c:v",
"libx264",
"-preset",
"ultrafast",
"-tune",
"zerolatency",
"-pix_fmt",
"yuv420p",
"-f",
"rtsp",
"-rtsp_transport",
"tcp",
&rtsp_url,
]);
match STREAM_CTRL.lock() { #[cfg(target_os = "linux")]
Ok(mut lock) => *lock = Some(stop_tx), cmd.args([
Err(e) => { "-f",
let _ = ctx.callback_data( "x11grab",
"VIDEO ERROR", "-framerate",
"Failed to acquire lock for stream control", "15",
e.to_string(), "-video_size",
); "1280x720",
return "stream control lock error"; "-i",
} ":0.0",
"-an",
"-c:v",
"libx264",
"-preset",
"ultrafast",
"-tune",
"zerolatency",
"-pix_fmt",
"yuv420p",
"-f",
"rtsp",
"-rtsp_transport",
"tcp",
&rtsp_url,
]);
spawn_ffmpeg_with_args(cmd, stop_rx, status_tx, format!("RTSP {}", rtsp_url));
if let Ok(mut lock) = STREAM_CTRL.lock() {
*lock = Some(stop_tx);
} }
match status_rx.recv_timeout(Duration::from_secs(2)) { match status_rx.recv_timeout(Duration::from_secs(2)) {
Ok(Ok(())) => { Ok(Ok(())) => {
let _ = ctx.callback_null("VIDEO", "FFmpeg started successfully"); let _ = ctx.callback_null("VIDEO", "FFmpeg RTSP stream started successfully");
"starting video stream" "starting video stream"
} }
Ok(Err(e)) => { Ok(Err(e)) => {
@@ -115,12 +147,87 @@ pub fn start_stream(
"ffmpeg did not respond" "ffmpeg did not respond"
} }
} }
}
pub fn start_rtp_stream(ctx: Context, address: String, port: String) -> &'static str {
stop_existing_stream();
let (stop_tx, stop_rx) = mpsc::channel();
let (status_tx, status_rx) = mpsc::channel();
let rtp_url = format!("rtp://{}:{}", address, port);
let mut cmd = Command::new("ffmpeg");
#[cfg(target_os = "windows")]
cmd.args([
"-f",
"gdigrab",
"-framerate",
"15",
"-i",
"desktop",
"-an",
"-c:v",
"libx264",
"-preset",
"ultrafast",
"-tune",
"zerolatency",
"-pix_fmt",
"yuv420p",
"-g",
"30",
"-f",
"rtp",
&rtp_url,
]);
#[cfg(target_os = "linux")]
cmd.args([
"-f",
"x11grab",
"-framerate",
"15",
"-video_size",
"1280x720",
"-i",
":0.0",
"-an",
"-c:v",
"libx264",
"-preset",
"ultrafast",
"-tune",
"zerolatency",
"-pix_fmt",
"yuv420p",
"-g",
"30",
"-f",
"rtp",
&rtp_url,
]);
spawn_ffmpeg_with_args(cmd, stop_rx, status_tx, format!("RTP {}", rtp_url));
if let Ok(mut lock) = STREAM_CTRL.lock() {
*lock = Some(stop_tx);
} }
#[cfg(not(any(target_os = "windows", target_os = "linux")))] match status_rx.recv_timeout(Duration::from_secs(2)) {
{ Ok(Ok(())) => {
ctx.callback_null("VIDEO ERROR", "Screen capture is only supported on Windows"); info!("Started RTP video stream toward {}", rtp_url);
"unsupported platform" let _ = ctx.callback_null("VIDEO", "FFmpeg RTP stream started successfully");
"starting RTP video stream"
}
Ok(Err(e)) => {
let _ = ctx.callback_data("VIDEO ERROR", "FFmpeg failed to start RTP stream", e);
"ffmpeg failed to start RTP stream"
}
Err(_) => {
let _ = ctx.callback_null("VIDEO ERROR", "FFmpeg RTP stream did not respond in time");
"ffmpeg RTP stream did not respond"
}
} }
} }

View File

@@ -1 +0,0 @@
{"v":1}

View File

@@ -1,6 +0,0 @@
{
"git": {
"sha1": "adfc323899e58f20c05ebb37595d5ca4fd09367f"
},
"path_in_vcs": "arma-rs-proc"
}

View File

@@ -1,42 +0,0 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "arma-rs-proc"
version = "1.11.1"
authors = ["Brett Mayson"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "proc macros for arma-rs"
readme = false
keywords = ["arma"]
license = "MIT"
repository = "https://github.com/brettmayson/arma-rs"
[lib]
name = "arma_rs_proc"
path = "src/lib.rs"
proc-macro = true
[dependencies.proc-macro2]
version = "1.0.92"
[dependencies.quote]
version = "1.0.37"
[dependencies.syn]
version = "2.0.90"
features = ["full"]

17
vendor/arma-rs-proc/Cargo.toml.orig generated vendored
View File

@@ -1,17 +0,0 @@
[package]
name = "arma-rs-proc"
description = "proc macros for arma-rs"
version = "1.11.1"
edition = "2021"
authors = ["Brett Mayson"]
repository = "https://github.com/brettmayson/arma-rs"
license = "MIT"
keywords = ["arma"]
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0.92"
quote = "1.0.37"
syn = { version = "2.0.90", features = ["full"] }

View File

@@ -1,145 +0,0 @@
use syn::{Error, Result};
use crate::derive::CombinedErrors;
pub struct ContainerAttributes {
pub transparent: Attribute<bool>,
pub default: Attribute<bool>,
}
impl Default for ContainerAttributes {
fn default() -> Self {
Self {
transparent: Attribute::new(false),
default: Attribute::new(false),
}
}
}
impl ParseAttr for ContainerAttributes {
fn parse_attr(&mut self, meta: syn::meta::ParseNestedMeta) -> Result<()> {
if meta.path.is_ident("transparent") {
return self.transparent.set(&meta, true);
}
if meta.path.is_ident("default") {
return self.default.set(&meta, true);
}
Err(meta.error(format!(
"unknown arma container attribute `{}`",
path_to_string(&meta.path)
)))
}
}
pub struct FieldAttributes {
pub default: Attribute<bool>,
pub from_str: Attribute<bool>,
pub to_string: Attribute<bool>,
}
impl Default for FieldAttributes {
fn default() -> Self {
Self {
default: Attribute::new(false),
from_str: Attribute::new(false),
to_string: Attribute::new(false),
}
}
}
impl ParseAttr for FieldAttributes {
fn parse_attr(&mut self, meta: syn::meta::ParseNestedMeta) -> Result<()> {
if meta.path.is_ident("default") {
return self.default.set(&meta, true);
}
if meta.path.is_ident("from_str") {
return self.from_str.set(&meta, true);
}
if meta.path.is_ident("to_string") {
return self.to_string.set(&meta, true);
}
Err(meta.error(format!(
"unknown arma field attribute `{}`",
path_to_string(&meta.path)
)))
}
}
pub trait ParseAttr {
fn parse_attr(&mut self, meta: syn::meta::ParseNestedMeta) -> Result<()>;
}
pub fn parse_attributes<T>(errors: &mut CombinedErrors, attrs: &[syn::Attribute]) -> T
where
T: ParseAttr + Default + Sized,
{
attrs.iter().fold(T::default(), |mut attributes, attr| {
if !attr.path().is_ident("arma") {
return attributes;
}
let result = attr.parse_nested_meta(|meta| {
if let Err(err) = attributes.parse_attr(meta) {
errors.add(err);
}
Ok(())
});
if let Err(err) = result {
errors.add(err);
}
attributes
})
}
pub struct Attribute<T> {
value: T,
path: Option<syn::Path>,
}
impl<T> Attribute<T> {
fn new(default: T) -> Self {
Self {
value: default,
path: None,
}
}
fn set(&mut self, meta: &syn::meta::ParseNestedMeta, value: T) -> Result<()> {
if self.is_set() {
return Err(meta.error(format!(
"duplicate arma attribute `{}`",
path_to_string(&meta.path)
)));
}
self.value = value;
self.path = Some(meta.path.clone());
Ok(())
}
pub fn is_set(&self) -> bool {
self.path.is_some()
}
pub fn value(&self) -> &T {
&self.value
}
#[must_use]
pub fn error(&self, message: &str) -> Error {
Error::new_spanned(self.path.as_ref().unwrap(), message)
}
}
fn path_to_string(path: &syn::Path) -> String {
path.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>()
.join("::")
}

View File

@@ -1,124 +0,0 @@
use proc_macro2::{Span, TokenStream};
use quote::ToTokens;
use syn::{Error, Result};
use crate::derive::{
attributes::{parse_attributes, ContainerAttributes, FieldAttributes},
r#struct, CombinedErrors,
};
pub struct ContainerData {
pub attributes: ContainerAttributes,
pub ident: syn::Ident,
pub generics: syn::Generics,
pub data: Data,
}
pub enum Data {
Struct(StructData),
}
pub enum StructData {
Map(Vec<FieldNamed>),
Tuple(Vec<FieldUnnamed>),
NewType(FieldUnnamed),
}
impl ContainerData {
pub fn from_input(errors: &mut CombinedErrors, input: syn::DeriveInput) -> Result<Self> {
let data = match input.data {
syn::Data::Struct(data) => Data::Struct(StructData::new(errors, data)?),
syn::Data::Enum(_) => Err(Error::new(Span::call_site(), "enums aren't supported"))?,
syn::Data::Union(_) => Err(Error::new(Span::call_site(), "unions aren't supported"))?,
};
let attributes = parse_attributes::<ContainerAttributes>(errors, &input.attrs);
Ok(Self {
attributes,
ident: input.ident,
generics: input.generics,
data,
})
}
pub fn validate_attributes(&self, errors: &mut CombinedErrors) {
match self.data {
Data::Struct(ref data) => {
r#struct::validate_attributes(errors, &self.attributes, data);
}
}
}
pub fn impl_into_arma(&self) -> TokenStream {
match self.data {
Data::Struct(ref data) => r#struct::impl_into_arma(&self.attributes, data),
}
}
pub fn impl_from_arma(&self) -> TokenStream {
match self.data {
Data::Struct(ref data) => r#struct::impl_from_arma(&self.attributes, data),
}
}
}
pub struct FieldNamed {
pub attributes: FieldAttributes,
pub ident: syn::Ident,
pub name: String,
pub _ty: syn::Type,
}
pub struct FieldUnnamed {
pub attributes: FieldAttributes,
pub index: syn::Index,
pub ty: syn::Type,
}
impl FieldNamed {
pub fn new(errors: &mut CombinedErrors, field: syn::Field) -> Self {
let ident = field.ident.unwrap();
let name = ident.to_string();
Self {
attributes: parse_attributes::<FieldAttributes>(errors, &field.attrs),
ident,
name,
_ty: field.ty,
}
}
}
impl FieldUnnamed {
pub fn new(errors: &mut CombinedErrors, field: syn::Field, index: usize) -> Self {
Self {
attributes: parse_attributes::<FieldAttributes>(errors, &field.attrs),
index: syn::Index::from(index),
ty: field.ty,
}
}
}
pub trait Field {
fn attributes(&self) -> &FieldAttributes;
fn token(&self) -> TokenStream;
}
impl Field for FieldNamed {
fn attributes(&self) -> &FieldAttributes {
&self.attributes
}
fn token(&self) -> TokenStream {
self.ident.to_token_stream()
}
}
impl Field for FieldUnnamed {
fn attributes(&self) -> &FieldAttributes {
&self.attributes
}
fn token(&self) -> TokenStream {
self.index.to_token_stream()
}
}

View File

@@ -1,72 +0,0 @@
mod attributes;
mod data;
mod r#struct;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, Result};
use data::ContainerData;
pub fn generate_from_arma(input: DeriveInput) -> Result<TokenStream> {
let container = parse_container_data(input)?;
let body = container.impl_from_arma();
let ident = container.ident;
let (impl_generics, ty_generics, where_clause) = container.generics.split_for_impl();
Ok(quote! {
#[automatically_derived]
impl #impl_generics arma_rs::FromArma for #ident #ty_generics #where_clause {
fn from_arma(func_input: String) -> Result<Self, arma_rs::FromArmaError> {
#body
}
}
})
}
pub fn generate_into_arma(input: DeriveInput) -> Result<TokenStream> {
let container = parse_container_data(input)?;
let body = container.impl_into_arma();
let ident = container.ident;
let (impl_generics, ty_generics, where_clause) = container.generics.split_for_impl();
Ok(quote! {
#[automatically_derived]
impl #impl_generics arma_rs::IntoArma for #ident #ty_generics #where_clause {
fn to_arma(&self) -> arma_rs::Value {
#body
}
}
})
}
fn parse_container_data(input: DeriveInput) -> Result<ContainerData> {
let mut errors = CombinedErrors::new();
let container = ContainerData::from_input(&mut errors, input)?;
container.validate_attributes(&mut errors);
errors.into_result().and(Ok(container))
}
pub struct CombinedErrors {
root: Option<syn::Error>,
}
impl CombinedErrors {
fn new() -> Self {
Self { root: None }
}
pub fn add(&mut self, error: syn::Error) {
match &mut self.root {
Some(root) => root.combine(error),
None => self.root = Some(error),
}
}
pub fn into_result(self) -> Result<()> {
match self.root {
Some(error) => Err(error),
None => Ok(()),
}
}
}

View File

@@ -1,162 +0,0 @@
use proc_macro2::TokenStream;
use quote::quote;
use crate::derive::{
attributes::ContainerAttributes,
data::{Field, FieldNamed, FieldUnnamed, StructData},
};
pub fn impl_from_arma(attributes: &ContainerAttributes, data: &StructData) -> TokenStream {
// For simplicity sake we assume that theres no conflicts and everything has already been validated
match &data {
StructData::Map(fields) => map_struct(attributes, fields),
StructData::Tuple(fields) => tuple_struct(attributes, fields),
StructData::NewType(field) => newtype_struct(attributes, field),
}
}
fn map_struct(attributes: &ContainerAttributes, fields: &[FieldNamed]) -> TokenStream {
if *attributes.transparent.value() {
return newtype_struct(attributes, fields.first().unwrap());
}
let mut setup = TokenStream::new();
setup.extend(quote! {
let mut input_as_values = std::collections::HashMap::<String, arma_rs::Value>::default();
let input_pairs: Vec<(String, arma_rs::Value)> = FromArma::from_arma(func_input)?;
for (k, v) in input_pairs {
if input_as_values.insert(k.clone(), v).is_some() {
return Err(arma_rs::FromArmaError::DuplicateField(k));
}
}
});
if *attributes.default.value() {
setup.extend(quote! {
let container_default: Self = std::default::Default::default();
});
};
let field_bodies = fields.iter().map(|field| {
let (ident, name) = (&field.ident, &field.name);
let some_match = if *field.attributes.from_str.value() {
quote!(input_value
.to_string()
.parse()
.map_err(arma_rs::FromArmaError::custom)?)
} else {
quote!(arma_rs::FromArma::from_arma(input_value.to_string())?)
};
let none_match = if *field.attributes.default.value() {
quote!(std::default::Default::default())
} else if *attributes.default.value() {
quote!(container_default.#ident)
} else {
quote!(return Err(arma_rs::FromArmaError::MissingField(#name.to_string())))
};
quote! {
#ident: match input_as_values.remove(#name) {
Some(input_value) => #some_match,
None => #none_match,
}
}
});
let check_unknown = quote! {
if let Some(unknown) = input_as_values.keys().next() {
return Err(arma_rs::FromArmaError::UnknownField(unknown.clone()));
}
};
quote! {
#setup
let result = Self {
#(#field_bodies),*
};
#check_unknown
Ok(result)
}
}
fn tuple_struct(attributes: &ContainerAttributes, fields: &[FieldUnnamed]) -> TokenStream {
let mut setup = TokenStream::new();
setup.extend(quote! {
let input_as_values: Vec<arma_rs::Value> = arma_rs::FromArma::from_arma(func_input)?;
let mut input_as_values = input_as_values.into_iter();
});
if *attributes.default.value() {
setup.extend(quote! {
let container_default: Self = std::default::Default::default();
});
};
let expected_len = fields.len();
let field_bodies = fields.iter().map(|field| {
let index = &field.index;
let some_match = if *field.attributes.from_str.value() {
quote!(input_value
.to_string()
.parse()
.map_err(arma_rs::FromArmaError::custom)?)
} else {
quote!(arma_rs::FromArma::from_arma(input_value.to_string())?)
};
let none_match = if *field.attributes.default.value() {
quote!(std::default::Default::default())
} else if *attributes.default.value() {
quote!(container_default.#index)
} else {
quote!(return Err(arma_rs::FromArmaError::InvalidLength {
expected: #expected_len,
actual: #index,
}))
};
quote! {
match input_as_values.next() {
Some(input_value) => #some_match,
None => #none_match,
}
}
});
let check_unknown = quote! {
let remaining = input_as_values.len();
if remaining > 0 {
return Err(arma_rs::FromArmaError::InvalidLength {
expected: #expected_len,
actual: #expected_len + remaining,
});
}
};
quote! {
#setup
let result = Self (
#(#field_bodies),*
);
#check_unknown
Ok(result)
}
}
fn newtype_struct(_attributes: &ContainerAttributes, field: &impl Field) -> TokenStream {
let token = field.token();
let field_body = if *field.attributes().from_str.value() {
quote!(func_input.parse().map_err(arma_rs::FromArmaError::custom)?)
} else {
quote!(arma_rs::FromArma::from_arma(func_input)?)
};
quote! {
Ok(Self {
#token: #field_body
})
}
}

View File

@@ -1,72 +0,0 @@
use proc_macro2::TokenStream;
use quote::quote;
use crate::derive::{
attributes::ContainerAttributes,
data::{Field, FieldNamed, FieldUnnamed, StructData},
};
pub fn impl_into_arma(attributes: &ContainerAttributes, data: &StructData) -> TokenStream {
// For simplicity sake we assume that theres no conflicts and everything has already been validated
match &data {
StructData::Map(fields) => map_struct(attributes, fields),
StructData::Tuple(fields) => tuple_struct(attributes, fields),
StructData::NewType(field) => newtype_struct(attributes, field),
}
}
fn map_struct(attributes: &ContainerAttributes, fields: &[FieldNamed]) -> TokenStream {
if *attributes.transparent.value() {
return newtype_struct(attributes, fields.first().unwrap());
}
let field_bodies = fields.iter().map(|field| {
let (ident, name) = (&field.ident, &field.name);
let (key, value) = if *field.attributes.to_string.value() {
(quote!(#name.to_string()), quote!(self.#ident.to_string()))
} else {
(quote!(#name.to_string()), quote!(self.#ident))
};
quote!((#key, arma_rs::IntoArma::to_arma(&#value)))
});
quote! {
std::collections::HashMap::<String, arma_rs::Value>::from([
#(#field_bodies),*
]).to_arma()
}
}
fn tuple_struct(_attributes: &ContainerAttributes, fields: &[FieldUnnamed]) -> TokenStream {
let field_bodies = fields.iter().map(|field| {
let index = &field.index;
if *field.attributes.to_string.value() {
quote!(self.#index.to_string())
} else {
quote!(self.#index)
}
});
quote! {
Vec::<arma_rs::Value>::from([
#(arma_rs::IntoArma::to_arma(&#field_bodies)),*
]).to_arma()
}
}
fn newtype_struct(_attributes: &ContainerAttributes, field: &impl Field) -> TokenStream {
let token = field.token();
let field_body = if *field.attributes().to_string.value() {
quote!(self.#token.to_string())
} else {
quote!(self.#token)
};
quote! {
arma_rs::IntoArma::to_arma(&#field_body)
}
}

View File

@@ -1,62 +0,0 @@
mod from;
mod into;
mod validate;
use proc_macro2::Span;
use syn::{Error, Result};
pub use from::impl_from_arma;
pub use into::impl_into_arma;
pub use validate::validate_attributes;
use crate::derive::{
data::{FieldNamed, FieldUnnamed, StructData},
CombinedErrors,
};
impl StructData {
pub fn new(errors: &mut CombinedErrors, data: syn::DataStruct) -> Result<Self> {
match data.fields {
syn::Fields::Unit => Err(Error::new(
Span::call_site(),
"unit-like structs aren't supported",
)),
syn::Fields::Named(fields) => {
if fields.named.is_empty() {
return Err(Error::new(
Span::call_site(),
"unit-like structs aren't supported",
));
}
let fields = fields
.named
.into_iter()
.map(|f| FieldNamed::new(errors, f))
.collect::<_>();
Ok(Self::Map(fields))
}
syn::Fields::Unnamed(fields) => {
if fields.unnamed.is_empty() {
return Err(Error::new(
Span::call_site(),
"unit-like structs aren't supported",
));
}
if fields.unnamed.len() == 1 {
let field = FieldUnnamed::new(errors, fields.unnamed[0].clone(), 0);
Ok(Self::NewType(field))
} else {
let fields = fields
.unnamed
.into_iter()
.enumerate()
.map(|(i, f)| FieldUnnamed::new(errors, f, i))
.collect::<_>();
Ok(Self::Tuple(fields))
}
}
}
}
}

View File

@@ -1,89 +0,0 @@
use syn::Error;
use crate::derive::{
attributes::{Attribute, ContainerAttributes, FieldAttributes},
data::StructData,
CombinedErrors,
};
pub fn validate_attributes(
errors: &mut CombinedErrors,
attributes: &ContainerAttributes,
data: &StructData,
) {
if *attributes.transparent.value() {
match data {
StructData::Map(fields) if fields.len() > 1 => {
errors.add(
attributes
.transparent
.error("#[arma(transparent)] structs must have exactly one field"),
);
}
StructData::Tuple(_) => {
errors.add(
attributes
.transparent
.error("#[arma(transparent)] cannot be used on tuple like structs"),
);
}
_ => {}
}
}
if let Some(attr) = get_default_attr(attributes, data) {
match data {
StructData::Map(_) if *attributes.transparent.value() => {
errors.add(
attr.error("#[arma(default)] and #[arma(transparent)] cannot be used together"),
);
}
StructData::NewType(_) => {
errors.add(attr.error("#[arma(default)] cannot be used on new type structs"));
}
_ => {}
}
}
if let StructData::Tuple(fields) = data {
let mut index_first_default = None;
for (index, field) in fields.iter().enumerate() {
match index_first_default {
None => {
if field.attributes.default.is_set() {
index_first_default = Some(index);
}
}
Some(index) => {
if !field.attributes.default.is_set() {
errors.add(Error::new_spanned(&field.ty,
format!("field must have #[arma(default)] because previous field {} has #[arma(default)]", index)
));
}
}
}
}
}
}
fn get_default_attr<'a>(
attributes: &'a ContainerAttributes,
data: &'a StructData,
) -> Option<&'a Attribute<bool>> {
if *attributes.default.value() {
return Some(&attributes.default);
}
field_attributes(data)
.iter()
.find(|attr| *attr.default.value())
.map(|f| &f.default)
}
fn field_attributes(data: &StructData) -> Vec<&FieldAttributes> {
match data {
StructData::Map(fields) => fields.iter().map(|f| &f.attributes).collect(),
StructData::Tuple(fields) => fields.iter().map(|f| &f.attributes).collect(),
StructData::NewType(field) => vec![&field.attributes],
}
}

View File

@@ -1,159 +0,0 @@
mod derive;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{DeriveInput, Error, ItemFn};
#[proc_macro_attribute]
/// Used to generate the necessary boilerplate for an Arma extension.
/// It should be applied to a function that takes no arguments and returns an extension.
pub fn arma(_attr: TokenStream, item: TokenStream) -> TokenStream {
let ast = syn::parse_macro_input!(item as ItemFn);
let init = ast.sig.ident.clone();
let extern_type = if cfg!(windows) { "stdcall" } else { "C" };
let ext_init = quote! {
if RV_EXTENSION.is_none() {
RV_EXTENSION = Some(#init());
}
};
#[cfg(all(target_os = "windows", target_arch = "x86"))]
let prefix = "safe32_";
#[cfg(not(all(target_os = "windows", target_arch = "x86")))]
let prefix = "";
macro_rules! fn_ident {
( $name:literal ) => {
Ident::new(&format!("{prefix}{}", $name), Span::call_site())
};
}
let versionfn = fn_ident!("RVExtensionVersion");
let noargfn = fn_ident!("RVExtension");
let argfn = fn_ident!("RVExtensionArgs");
let callbackfn = fn_ident!("RVExtensionRegisterCallback");
let contextfn = fn_ident!("RVExtensionContext");
TokenStream::from(quote! {
use arma_rs::libc as arma_rs_libc;
static mut RV_EXTENSION: Option<Extension> = None;
#[cfg(all(target_os="windows", target_arch="x86"))]
arma_rs::link_args::windows! {
unsafe {
raw("/EXPORT:_RVExtensionVersion@8=_safe32_RVExtensionVersion@8");
raw("/EXPORT:_RVExtension@12=_safe32_RVExtension@12");
raw("/EXPORT:_RVExtensionArgs@20=_safe32_RVExtensionArgs@20");
raw("/EXPORT:_RVExtensionRegisterCallback@4=_safe32_RVExtensionRegisterCallback@4");
raw("/EXPORT:_RVExtensionContext@8=_safe32_RVExtensionContext@8");
}
}
/// Returns extension version, called by Arma on extension load.
/// This function is generated by the [`arma_rs::arma`] proc macro.
#[no_mangle]
#[doc(hidden)]
pub unsafe extern #extern_type fn #versionfn(output: *mut arma_rs_libc::c_char, size: arma_rs_libc::size_t) -> arma_rs_libc::c_int {
#ext_init
if let Some(ext) = &RV_EXTENSION {
arma_rs::write_cstr(ext.version().to_string(), output, size);
}
0
}
/// Run extension function, called by Arma on `callExtension` without arguments.
/// This function is generated by the [`arma_rs::arma`] proc macro.
#[no_mangle]
#[doc(hidden)]
pub unsafe extern #extern_type fn #noargfn(output: *mut arma_rs_libc::c_char, size: arma_rs_libc::size_t, function: *mut arma_rs_libc::c_char) {
#ext_init
if let Some(ext) = &RV_EXTENSION {
if ext.allow_no_args() {
ext.handle_call(function, output, size, None, None, true);
}
}
}
/// Run extension function with arguments, called by Arma on `callExtension` with arguments.
/// This function is generated by the [`arma_rs::arma`] proc macro.
#[no_mangle]
#[doc(hidden)]
pub unsafe extern #extern_type fn #argfn(output: *mut arma_rs_libc::c_char, size: arma_rs_libc::size_t, function: *mut arma_rs_libc::c_char, args: *mut *mut arma_rs_libc::c_char, arg_count: arma_rs_libc::c_int) -> arma_rs_libc::c_int {
#ext_init
if let Some(ext) = &RV_EXTENSION {
ext.handle_call(function, output, size, Some(args), Some(arg_count), true)
} else {
0
}
}
/// Set extension callback, called by Arma on extension load.
/// This function is generated by the [`arma_rs::arma`] proc macro.
#[no_mangle]
#[doc(hidden)]
pub unsafe extern #extern_type fn #callbackfn(callback: arma_rs::Callback) {
#ext_init
if let Some(ext) = &mut RV_EXTENSION {
ext.register_callback(callback);
ext.run_callbacks();
}
}
/// Provide extension call context, called by Arma on `callExtension`.
/// This function is generated by the [`arma_rs::arma`] proc macro.
#[no_mangle]
#[doc(hidden)]
pub unsafe extern #extern_type fn #contextfn(args: *mut *mut arma_rs_libc::c_char, arg_count: arma_rs_libc::c_int) {
#ext_init
if let Some(ext) = &mut RV_EXTENSION {
ext.handle_call_context(args, arg_count);
}
}
#ast
})
}
/// Derive implementation of `FromArma`, only supports structs.
/// - Map structs are converted from an hashmap.
/// - Tuple structs are converted from an array.
/// - Newtype structs directly use's the value's `FromArma` implementation.
/// - Unit-like structs are not supported.
///
/// ### Container Attributes
/// - `#[arma(transparent)]`: treat single field map structs as if its a newtype structs.
/// - `#[arma(default)]`: any missing field will be filled by the structs `Default` implementation.
///
/// ### Field Attributes
/// - `#[arma(from_str)]`: use the types `std::str::FromStr` instead of `FromArma`.
/// - `#[arma(default)]`: if missing use its `Default` implementation (takes precedence over container).
#[proc_macro_derive(FromArma, attributes(arma))]
pub fn derive_from_arma(item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as DeriveInput);
derive::generate_from_arma(input)
.unwrap_or_else(Error::into_compile_error)
.into()
}
/// Derive implementation of `IntoArma`, only supports structs.
/// - Map structs are converted to an hashmap.
/// - Tuple structs are converted to an array.
/// - Newtype structs directly use's the value's `IntoArma` implementation.
/// - Unit-like structs are not supported.
///
/// ### Container Attributes
/// - `#[arma(transparent)]`: treat single field map structs as if its a newtype structs.
///
/// ### Field Attributes
/// - `#[arma(to_string)]`: use the types `std::string::ToString` instead of `IntoArma`.
#[proc_macro_derive(IntoArma, attributes(arma))]
pub fn derive_into_arma(item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as DeriveInput);
derive::generate_into_arma(input)
.unwrap_or_else(Error::into_compile_error)
.into()
}

View File

@@ -1 +0,0 @@
{"v":1}

View File

@@ -1,6 +0,0 @@
{
"git": {
"sha1": "6cac89d7a9d02027bb85a6fa583b3ef0e8bf0f5a"
},
"path_in_vcs": "arma-rs"
}

951
vendor/arma-rs/Cargo.lock generated vendored
View File

@@ -1,951 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "arma-rs"
version = "1.11.14"
dependencies = [
"arma-rs-proc",
"chrono",
"crossbeam-channel",
"libc",
"link_args",
"log",
"seq-macro",
"serde",
"serde_json",
"state",
"trybuild",
"uuid",
"winapi",
"windows 0.61.1",
]
[[package]]
name = "arma-rs-proc"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf67c0d0c7a59275e5ac4f3fce0cbdbcf3ba12e47bc30be6a3327d6a1bc151f8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "cc"
version = "1.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "crossbeam-channel"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "generator"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
dependencies = [
"cc",
"libc",
"log",
"rustversion",
"windows 0.48.0",
]
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indexmap"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "link_args"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c7721e472624c9aaad27a5eb6b7c9c6045c7a396f2efb6dabaec1b640d5e89b"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "loom"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"serde",
"serde_json",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "seq-macro"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "state"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8"
dependencies = [
"loom",
]
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "target-triple"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790"
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "toml"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "tracing"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
name = "trybuild"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ae08be68c056db96f0e6c6dd820727cca756ced9e1f4cc7fdd20e2a55e23898"
dependencies = [
"glob",
"serde",
"serde_derive",
"serde_json",
"target-triple",
"termcolor",
"toml",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "uuid"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows"
version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-link",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-numerics"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
dependencies = [
"memchr",
]

View File

@@ -1,108 +0,0 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "arma-rs"
version = "1.11.14"
authors = ["Brett Mayson"]
build = "build.rs"
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Arma 3 Extensions in Rust"
readme = "README.md"
keywords = ["arma"]
license = "MIT"
repository = "https://github.com/brettmayson/arma-rs"
[features]
default = ["extension"]
extension = [
"libc",
"crossbeam-channel",
]
[lib]
name = "arma_rs"
path = "src/lib.rs"
[[example]]
name = "hello_world"
path = "examples/hello_world.rs"
[[test]]
name = "derive"
path = "tests/derive.rs"
[[test]]
name = "emulate"
path = "tests/emulate.rs"
[[test]]
name = "main"
path = "tests/main.rs"
[dependencies.arma-rs-proc]
version = "1.11.1"
[dependencies.chrono]
version = "0.4.40"
optional = true
[dependencies.crossbeam-channel]
version = "0.5.14"
optional = true
[dependencies.libc]
version = "0.2.171"
optional = true
[dependencies.log]
version = "0.4.27"
[dependencies.seq-macro]
version = "0.3.6"
[dependencies.serde]
version = "1.0.219"
features = ["derive"]
optional = true
[dependencies.serde_json]
version = "1.0.140"
optional = true
[dependencies.state]
version = "0.6.0"
[dependencies.uuid]
version = "1.16.0"
optional = true
[dev-dependencies.trybuild]
version = "1.0.104"
[target.'cfg(all(target_os="windows", target_arch="x86"))'.dependencies.link_args]
version = "0.6.0"
[target."cfg(windows)".dependencies.winapi]
version = "0.3.9"
features = ["libloaderapi"]
[target."cfg(windows)".dependencies.windows]
version = "0.61.1"
features = [
"Win32_Foundation",
"Win32_System_Console",
]

41
vendor/arma-rs/Cargo.toml.orig generated vendored
View File

@@ -1,41 +0,0 @@
[package]
name = "arma-rs"
description = "Arma 3 Extensions in Rust"
version = "1.11.14"
edition = "2021"
authors = ["Brett Mayson"]
repository = "https://github.com/brettmayson/arma-rs"
license = "MIT"
keywords = ["arma"]
readme = "../README.md"
[dependencies]
arma-rs-proc = { path = "../arma-rs-proc", version = "1.11.1" }
log = "0.4.27"
state = "0.6.0"
seq-macro = "0.3.6"
chrono = { version = "0.4.40", optional = true }
crossbeam-channel = { version = "0.5.14", optional = true }
libc = { version = "0.2.171", optional = true }
serde = { version = "1.0.219", features = ["derive"], optional = true }
serde_json = { version = "1.0.140", optional = true }
uuid = { version = "1.16.0", optional = true }
[target.'cfg(all(target_os="windows", target_arch="x86"))'.dependencies]
link_args = "0.6.0"
[target.'cfg(windows)'.dependencies.winapi]
version = "0.3.9"
features = ["libloaderapi"]
[target.'cfg(windows)'.dependencies.windows]
version = "0.61.1"
features = ["Win32_Foundation", "Win32_System_Console"]
[dev-dependencies]
trybuild = "1.0.104"
[features]
default = ["extension"]
extension = ["libc", "crossbeam-channel"]

View File

@@ -1,439 +0,0 @@
# arma-rs
[Join the arma-rs Discord!](https://discord.gg/qXWUrrwy5d)
[![codecov](https://codecov.io/gh/BrettMayson/arma-rs/branch/main/graph/badge.svg?token=A1H7SEZ434)](https://codecov.io/gh/BrettMayson/arma-rs)
The best way to make Arma 3 Extensions.
## Usage
```toml
[dependencies]
arma-rs = "1.11.10"
[lib]
name = "my_extension"
crate-type = ["cdylib"]
```
### Hello World
```rust
use arma_rs::{arma, Extension};
#[arma]
fn init() -> Extension {
Extension::build()
.command("hello", hello)
.command("welcome", welcome)
.finish()
}
pub fn hello() -> &'static str {
"Hello"
}
pub fn welcome(name: String) -> String {
format!("Welcome {}", name)
}
```
```sqf
"my_extension" callExtension ["hello", []]; // Returns ["Hello", 0, 0]
"my_extension" callExtension ["welcome", ["John"]]; // Returns ["Welcome John", 0, 0]
```
## Command Groups
Commands can be grouped together, making your large projects much easier to manage.
```rust
use arma_rs::{arma, Extension, Group};
#[arma]
fn init() -> Extension {
Extension::build()
.group("hello",
Group::new()
.command("english", hello::english)
.group("english",
Group::new()
.command("casual", hello::english_casual)
)
.command("french", hello::french),
)
.group("welcome",
Group::new()
.command("english", welcome::english)
.command("french", welcome::french),
)
.finish()
}
mod hello {
pub fn english() -> &'static str {
"Hello"
}
pub fn english_casual() -> &'static str {
"Hey"
}
pub fn french() -> &'static str {
"Bonjour"
}
}
mod welcome {
pub fn english(name: String) -> String {
format!("Welcome {}", name)
}
pub fn french(name: String) -> String {
format!("Bienvenue {}", name)
}
}
```
Commands groups are called by using the format `group:command`. You can nest groups as much as you want.
```sqf
"my_extension" callExtension ["hello:english", []]; // Returns ["Hello", 0, 0]
"my_extension" callExtension ["hello:english:casual", []]; // Returns ["Hey", 0, 0]
"my_extension" callExtension ["hello:french", []]; // Returns ["Bonjour", 0, 0]
```
## Callbacks
Extension callbacks can be invoked anywhere in the extension by adding a variable of type `Context` to the start of a handler.
```rust
use arma_rs::Context;
pub fn sleep(ctx: Context, duration: u64, id: String) {
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(duration));
ctx.callback_data("example_timer", "done", Some(id));
});
}
pub fn group() -> arma_rs::Group {
arma_rs::Group::new().command("sleep", sleep)
}
```
## Call Context
Since Arma v2.11 additional context is provided each time the extension is called. This context can be accessed through the optional `ArmaCallContext` argument.
Since Arma v2.18 the context is only requested from Arma when the functionh has `ArmaCallContext` as an argument.
```rust
use arma_rs::{CallContext, CallContextStackTrace};
pub fn call_context(call_context: CallContext) -> String {
format!(
"{:?},{:?},{:?},{:?},{:?}",
call_context.caller(),
call_context.source(),
call_context.mission(),
call_context.server(),
call_context.remote_exec_owner(),
)
}
pub fn stack_trace(call_context: CallContextStackTrace) -> String {
format!(
"{:?}\n{:?}",
call_context.source(),
call_context.stack_trace()
)
}
pub fn group() -> arma_rs::Group {
arma_rs::Group::new()
.command("call_context", call_context)
.command("stack_trace", stack_trace)
}
```
## Persistent State
Both the extension and command groups allow for type based persistent state values with at most one instance per type. These state values can then be accessed through the optional `Context` argument.
### Global State
Extension state is accessible from any command handler.
```rust
use arma_rs::{arma, Context, ContextState, Extension};
use std::sync::atomic::{AtomicU32, Ordering};
#[arma]
fn init() -> Extension {
Extension::build()
.command("counter_increment", increment)
.state(AtomicU32::new(0))
.finish()
}
pub fn increment(ctx: Context) -> Result<(), ()> {
let Some(counter) = ctx.global().get::<AtomicU32>() else {
return Err(());
};
counter.fetch_add(1, Ordering::SeqCst);
Ok(())
}
```
### Group State
Command group state is only accessible from command handlers within the same group.
```rust
use arma_rs::{Context, ContextState, Extension};
use std::sync::atomic::{AtomicU32, Ordering};
pub fn increment(ctx: Context) -> Result<(), ()> {
let Some(counter) = ctx.group().get::<AtomicU32>() else {
return Err(());
};
counter.fetch_add(1, Ordering::SeqCst);
Ok(())
}
pub fn group() -> arma_rs::Group {
arma_rs::Group::new()
.command("increment", increment)
.state(AtomicU32::new(0))
}
```
## Custom Types
If you're bringing your existing Rust library with your own types, you can easily define how they are converted to and from Arma.
```rust
use arma_rs::{FromArma, IntoArma, Value, FromArmaError};
pub struct MemoryReport {
total: u64,
free: u64,
avail: u64,
}
impl FromArma for MemoryReport {
fn from_arma(s: String) -> Result<Self, FromArmaError> {
let (total, free, avail) = <(u64, u64, u64)>::from_arma(s)?;
Ok(Self { total, free, avail })
}
}
impl IntoArma for MemoryReport {
fn to_arma(&self) -> Value {
Value::Array(
vec![self.total, self.free, self.avail]
.into_iter()
.map(|v| v.to_string().to_arma())
.collect(),
)
}
}
```
### Derive
Alternatively you can derive these traits. Note that the derive and manual implementation examples slightly differ, as when deriving map like structs its represented as an hashmap rather than an array. For more information on data representation and attributes see: [FromArma](https://docs.rs/arma-rs/latest/arma_rs/derive.FromArma.html) and [IntoArma](https://docs.rs/arma-rs/latest/arma_rs/derive.IntoArma.html).
```rust
use arma_rs::{FromArma, IntoArma};
#[derive(FromArma, IntoArma)]
struct MemoryReport {
#[arma(to_string)]
total: u64,
#[arma(to_string)]
free: u64,
#[arma(to_string)]
avail: u64,
}
```
Deriving is currently only supported for structs, this might change in the future.
## Error Codes
By default arma-rs will only allow commands via `RvExtensionArgs`. Using `callExtension` with only a function name will return an empty string.
```sqf
"my_extension" callExtension "hello:english" // returns ""
"my_extension" callExtension ["hello:english", []] // returns ["Hello", 0, 0]
```
This behaviour can be changed by calling `.allow_no_args()` when building the extension. It is recommended not to use this, and to implement error handling instead.
| Code | Description |
|------|---------------------------------------------------|
| 0 | Success |
| 1 | Command not found |
| 2x | Invalid argument count, x is received count |
| 3x | Invalid argument type, x is argument position |
| 4 | Attempted to write a value larger than the buffer |
| 9 | Application error, from using a Result |
### Error Examples
```rust
use arma_rs::Context;
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn overflow(ctx: Context) -> String {
"X".repeat(ctx.buffer_len() + 1)
}
pub fn should_error(error: bool) -> Result<String, String> {
if error {
Err(String::from("told to error"))
} else {
Ok(String::from("told to succeed"))
}
}
```
```sqf
"my_extension" callExtension ["add", [1, 2]]; // Returns ["3", 0, 0]
"my_extension" callExtension ["sub", [1, 2]]; // Returns ["", 1, 0]
"my_extension" callExtension ["add", [1, 2, 3]]; // Returns ["", 23, 0], didn't expect 3 elements
"my_extension" callExtension ["add", [1, "two"]]; // Returns ["", 31, 0], unable to parse the second argument
"my_extension" callExtension ["overflow", []]; // Returns ["", 4, 0], the return size was larger than the buffer
"my_extension" callExtension ["should_error", [true]]; // Returns ["told to error", 9, 0]
"my_extension" callExtension ["should_error", [false]]; // Returns ["told to succeed", 0, 0]
```
## Testing
Tests can be created utilizing the `extension.call()` method.
```rust,ignore
mod tests {
#[test]
fn hello() {
let extension = init().testing();
let (output, _) = extension.call("hello:english", None);
assert_eq!(output, "hello");
}
#[test]
fn welcome() {
let extension = init().testing();
let (output, _) =
extension.call("welcome:english", Some(vec!["John".to_string()]));
assert_eq!(output, "Welcome John");
}
#[test]
fn sleep_1sec() {
let extension = Extension::build()
.group("timer", super::group())
.finish()
.testing();
let (_, code) = extension.call(
"timer:sleep",
Some(vec!["1".to_string(), "test".to_string()]),
);
assert_eq!(code, 0);
let result = extension.callback_handler(
|name, func, data| {
assert_eq!(name, "timer:sleep");
assert_eq!(func, "done");
if let Some(Value::String(s)) = data {
Result::Ok(s)
} else {
Result::Err("Data was not a string".to_string())
}
},
Duration::from_secs(2),
);
assert_eq!(Result::Ok("test".to_string()), result);
}
}
```
## Unit Loadout Array
arma-rs includes a [loadout module](https://docs.rs/arma-rs/latest/arma_rs/loadout/index.html) to assist with the handling of [Arma's Unit Loadout Array](https://community.bistudio.com/wiki/Unit_Loadout_Array).
```rust
use arma_rs::{FromArma, loadout::{Loadout, InventoryItem, Weapon, Magazine}};
let l = r#"[[],[],[],["U_Marshal",[]],[],[],"H_Cap_headphones","G_Aviator",[],["ItemMap","ItemGPS","","ItemCompass","ItemWatch",""]]"#;
let mut loadout = Loadout::from_arma(l.to_string()).unwrap();
loadout.set_secondary({
let mut weapon = Weapon::new("launch_B_Titan_short_F".to_string());
weapon.set_primary_magazine(Magazine::new("Titan_AT".to_string(), 1));
weapon
});
loadout.set_primary({
let mut weapon = Weapon::new("arifle_MXC_F".to_string());
weapon.set_optic("optic_Holosight".to_string());
weapon
});
let uniform = loadout.uniform_mut();
uniform.set_class("U_B_CombatUniform_mcam".to_string());
let uniform_items = uniform.items_mut().unwrap();
uniform_items.push(InventoryItem::new_item("FirstAidKit".to_string(), 3));
uniform_items.push(InventoryItem::new_magazine("30Rnd_65x39_caseless_mag".to_string(), 5, 30));
```
## Common Rust Libraries
arma-rs supports some common Rust libraries.
You can enable their support by adding their name to the features of arma-rs.
```toml
arma-rs = { version = "1.8.0", features = ["chrono"] }
```
Please create an issue first if you would like to add support for a new library.
### chrono
[`crates.io`](https://crates.io/crates/chrono)
#### chrono - Convert to Arma
[`NaiveDateTime`](https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDateTime.html) and [`DateTime<TimeZone>`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) will be converted to [Arma's date array](https://community.bistudio.com/wiki/systemTimeUTC).
The timezone will always be converted to UTC.
#### chrono - Convert From Arma
[Arma's date array](https://community.bistudio.com/wiki/systemTimeUTC) can be converted to [`NaiveDateTime`](https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDateTime.html).
### uuid
[`crates.io`](https://crates.io/crates/uuid)
#### uuid - Convert To Arma
[`Uuid`](https://docs.rs/uuid/latest/uuid/struct.Uuid.html) will be converted to a string.
### serde_json
[`crates.io`](https://crates.io/crates/serde_json)
#### serde_json - Convert To Arma
Any variant of [`serde_json::Value`](https://docs.serde.rs/serde_json/enum.Value.html) will be converted to the appropriate Arma type.
## Building for x86 (32 Bit)
```sh
rustup toolchain install stable-i686-pc-windows-msvc
cargo +stable-i686-pc-windows-msvc build
```
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

View File

@@ -1,16 +0,0 @@
use std::path::Path;
fn main() {
let mut root = Path::new("../../README.md");
if !root.exists() {
root = Path::new("../README.md");
}
if !root.exists() {
root = Path::new("README.md");
}
std::fs::copy(
root,
Path::new(&format!("{}/README.md", std::env::var("OUT_DIR").unwrap())),
)
.unwrap();
}

Some files were not shown because too many files have changed in this diff Show More