From 5fa1a2684375f1ef4c3c0f20b6aa228752a58ed0 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Fri, 8 Aug 2025 11:06:53 +0200 Subject: [PATCH 1/4] Add async callbacks and Kronos integration to API Introduces async callback support for command execution in spawn methods, adds registration/unregistration for update and startup callbacks, and improves logging and signal handling. Adds a new Kronos module and main entry point for initializing and running the API with Kronos integration. Refactors example scripts and updates VSCode launch configurations for new entry points. --- scripts/python/API/.vscode/launch.json | 16 +- scripts/python/API/api.py | 331 ++++++++++-------- .../API/example_disembarked_infantry.py | 2 +- ...ce_control.py => example_voice_control.py} | 2 +- scripts/python/API/kronos/kronos.py | 16 + scripts/python/API/main.py | 20 ++ 6 files changed, 244 insertions(+), 143 deletions(-) rename scripts/python/API/{voice_control.py => example_voice_control.py} (98%) create mode 100644 scripts/python/API/kronos/kronos.py create mode 100644 scripts/python/API/main.py diff --git a/scripts/python/API/.vscode/launch.json b/scripts/python/API/.vscode/launch.json index 96695e79..59e84c31 100644 --- a/scripts/python/API/.vscode/launch.json +++ b/scripts/python/API/.vscode/launch.json @@ -5,18 +5,26 @@ "version": "0.2.0", "configurations": [ { - "name": "Voice control", + "name": "Python: Main", "type": "debugpy", "request": "launch", - "program": "voice_control.py", + "program": "${workspaceFolder}/main.py", "console": "integratedTerminal", "justMyCode": false, }, { - "name": "Test bed", + "name": "Example voice control", "type": "debugpy", "request": "launch", - "program": "testbed.py", + "program": "example_voice_control.py", + "console": "integratedTerminal", + "justMyCode": false, + }, + { + "name": "Example disembarked infantry", + "type": "debugpy", + "request": "launch", + "program": "example_disembarked_infantry.py", "console": "integratedTerminal", "justMyCode": false, } diff --git a/scripts/python/API/api.py b/scripts/python/API/api.py index 545fc23e..60a97f3f 100644 --- a/scripts/python/API/api.py +++ b/scripts/python/API/api.py @@ -119,7 +119,122 @@ class API: signal.signal(signal.SIGINT, signal_handler) # Ctrl+C if hasattr(signal, 'SIGTERM'): signal.signal(signal.SIGTERM, signal_handler) # Termination signal + + async def _check_command_executed(self, command_hash: str, execution_callback, wait_for_result: bool, max_wait_time: int = 60): + """ + Check if a command has been executed by polling the API. + """ + start_time = time.time() + while True: + response = self._get(f"commands?commandHash={command_hash}") + if response.status_code == 200: + try: + data = response.json() + if data.get("commandExecuted") == True and (data.get("commandResult") is not None or (not wait_for_result)): + self.logger.info(f"Command {command_hash} executed successfully, command result: {data.get('commandResult')}") + if execution_callback: + await execution_callback(data.get("commandResult")) + break + elif data.get("status") == "failed": + self.logger.error(f"Command {command_hash} failed to execute.") + break + except ValueError: + self.logger.error("Failed to parse JSON response") + if time.time() - start_time > max_wait_time: + self.logger.warning(f"Timeout: Command {command_hash} did not complete within {max_wait_time} seconds.") + break + await asyncio.sleep(1) + async def _run_callback_async(self, callback, *args): + """ + Run a callback asynchronously, handling both sync and async callbacks. + """ + try: + if asyncio.iscoroutinefunction(callback): + await callback(*args) + else: + callback(*args) + except Exception as e: + # Log the error but don't crash the update process + self.logger.error(f"Error in callback: {e}") + + def register_on_update_callback(self, callback): + """ + Register a callback function to be called on each update. + + Args: + callback (function): The function to call on update. Can be sync or async. + The function should accept a single argument, which is the API instance. + """ + self.on_update_callback = callback + + async def _run_async(self): + """ + Async implementation of the API service loop. + """ + # Setup signal handlers for graceful shutdown + self._setup_signal_handlers() + + # Here you can add any initialization logic if needed + self.logger.info("API started") + self.logger.info("Press Ctrl+C to stop gracefully") + + self.running = True + self.should_stop = False + + # Call the startup callback if registered + if self.on_startup_callback: + try: + await self._run_callback_async(self.on_startup_callback, self) + except Exception as e: + self.logger.error(f"Error in startup callback: {e}") + + try: + while not self.should_stop: + # Update units from the last update timestamp + self.update_units(self.units_update_timestamp) + + if self.on_update_callback: + await self._run_callback_async(self.on_update_callback, self) + await asyncio.sleep(self.interval) + except KeyboardInterrupt: + self.logger.info("Keyboard interrupt received") + self.stop() + finally: + self.logger.info("API stopped") + self.running = False + + def unregister_on_update_callback(self): + """ + Unregister the callback function that is called on each update. + """ + self.on_update_callback = None + + def register_on_startup_callback(self, callback): + """ + Register a callback function to be called on startup. + Args: + callback (function): The function to call on startup. Can be sync or async. + The function should accept a single argument, which is the API instance. + """ + self.on_startup_callback = callback + + def unregister_on_startup_callback(self): + """ + Unregister the callback function that is called on startup. + """ + self.on_startup_callback = None + + def set_log_level(self, level): + """ + Set the logging level for the API. + + Args: + level: Logging level (e.g., logging.DEBUG, logging.INFO, logging.WARNING, self.logger.error) + """ + self.logger.setLevel(level) + self.logger.info(f"Log level set to {logging.getLevelName(level)}") + def get_units(self): """ Get all units from the API. Notice that if the API is not running, update_units() must be manually called first. @@ -170,8 +285,7 @@ class API: self.logger.error("Failed to parse JSON response") else: self.logger.error(f"Failed to fetch units: {response.status_code} - {response.text}") - - + def update_logs(self, time = 0): """ Fetch the logs from the API. @@ -192,7 +306,7 @@ class API: else: self.logger.error(f"Failed to fetch logs: {response.status_code} - {response.text}") - def spawn_aircrafts(self, units: list[UnitSpawnTable], coalition: str, airbaseName: str, country: str, immediate: bool, spawnPoints: int = 0): + def spawn_aircrafts(self, units: list[UnitSpawnTable], coalition: str, airbaseName: str, country: str, immediate: bool, spawnPoints: int = 0, execution_callback=None): """ Spawn aircraft units at the specified location or airbase. Args: @@ -202,6 +316,7 @@ class API: country (str): The country of the units. immediate (bool): Whether to spawn the units immediately or not, overriding the scheduler. spawnPoints (int): Amount of spawn points to use, default is 0. + execution_callback (function): An optional async callback function to execute after the command is processed. """ command = { "units": [unit.toJSON() for unit in units], @@ -214,7 +329,21 @@ class API: data = { "spawnAircrafts": command } response = self._put(data) - def spawn_helicopters(self, units: list[UnitSpawnTable], coalition: str, airbaseName: str, country: str, immediate: bool, spawnPoints: int = 0): + # Parse the response as JSON if callback is provided + if execution_callback: + try: + response_data = response.json() + command_hash = response_data.get("commandHash", None) + if command_hash: + self.logger.info(f"Aircraft spawned successfully. Command Hash: {command_hash}") + # Start a background task to check if the command was executed + asyncio.create_task(self._check_command_executed(command_hash, execution_callback, wait_for_result=True)) + else: + self.logger.error("Command hash not found in response") + except ValueError: + self.logger.error("Failed to parse JSON response") + + def spawn_helicopters(self, units: list[UnitSpawnTable], coalition: str, airbaseName: str, country: str, immediate: bool, spawnPoints: int = 0, execution_callback=None): """ Spawn helicopter units at the specified location or airbase. Args: @@ -224,6 +353,7 @@ class API: country (str): The country of the units. immediate (bool): Whether to spawn the units immediately or not, overriding the scheduler. spawnPoints (int): Amount of spawn points to use, default is 0. + execution_callback (function): An optional async callback function to execute after the command is processed. """ command = { "units": [unit.toJSON() for unit in units], @@ -236,6 +366,20 @@ class API: data = { "spawnHelicopters": command } response = self._put(data) + # Parse the response as JSON if callback is provided + if execution_callback: + try: + response_data = response.json() + command_hash = response_data.get("commandHash", None) + if command_hash: + self.logger.info(f"Helicopters spawned successfully. Command Hash: {command_hash}") + # Start a background task to check if the command was executed + asyncio.create_task(self._check_command_executed(command_hash, execution_callback, wait_for_result=True)) + else: + self.logger.error("Command hash not found in response") + except ValueError: + self.logger.error("Failed to parse JSON response") + def spawn_ground_units(self, units: list[UnitSpawnTable], coalition: str, country: str, immediate: bool, spawnPoints: int, execution_callback): """ Spawn ground units at the specified location. @@ -272,32 +416,7 @@ class API: except ValueError: self.logger.error("Failed to parse JSON response") - async def _check_command_executed(self, command_hash: str, execution_callback, wait_for_result: bool, max_wait_time: int = 60): - """ - Check if a command has been executed by polling the API. - """ - start_time = time.time() - while True: - response = self._get(f"commands?commandHash={command_hash}") - if response.status_code == 200: - try: - data = response.json() - if data.get("commandExecuted") == True and (data.get("commandResult") is not None or (not wait_for_result)): - self.logger.info(f"Command {command_hash} executed successfully, command result: {data.get('commandResult')}") - if execution_callback: - await execution_callback(data.get("commandResult")) - break - elif data.get("status") == "failed": - self.logger.error(f"Command {command_hash} failed to execute.") - break - except ValueError: - self.logger.error("Failed to parse JSON response") - if time.time() - start_time > max_wait_time: - self.logger.warning(f"Timeout: Command {command_hash} did not complete within {max_wait_time} seconds.") - break - await asyncio.sleep(1) - - def spawn_navy_units(self, units: list[UnitSpawnTable], coalition: str, country: str, immediate: bool, spawnPoints: int = 0): + def spawn_navy_units(self, units: list[UnitSpawnTable], coalition: str, country: str, immediate: bool, spawnPoints: int = 0, execution_callback=None): """ Spawn navy units at the specified location. Args: @@ -306,6 +425,7 @@ class API: country (str): The country of the units. immediate (bool): Whether to spawn the units immediately or not, overriding the scheduler. spawnPoints (int): Amount of spawn points to use, default is 0. + execution_callback (function): An optional async callback function to execute after the command is processed. """ command = { "units": [unit.toJSON() for unit in units], @@ -316,6 +436,20 @@ class API: } data = { "spawnNavyUnits": command } response = self._put(data) + + # Parse the response as JSON if callback is provided + if execution_callback: + try: + response_data = response.json() + command_hash = response_data.get("commandHash", None) + if command_hash: + self.logger.info(f"Navy units spawned successfully. Command Hash: {command_hash}") + # Start a background task to check if the command was executed + asyncio.create_task(self._check_command_executed(command_hash, execution_callback, wait_for_result=True)) + else: + self.logger.error("Command hash not found in response") + except ValueError: + self.logger.error("Failed to parse JSON response") def create_radio_listener(self): """ @@ -327,55 +461,6 @@ class API: from radio.radio_listener import RadioListener return RadioListener(self, "localhost", self.config.get("audio").get("WSPort")) - def register_on_update_callback(self, callback): - """ - Register a callback function to be called on each update. - - Args: - callback (function): The function to call on update. Can be sync or async. - The function should accept a single argument, which is the API instance. - """ - self.on_update_callback = callback - - def register_on_startup_callback(self, callback): - """ - Register a callback function to be called on startup. - Args: - callback (function): The function to call on startup. Can be sync or async. - The function should accept a single argument, which is the API instance. - """ - self.on_startup_callback = callback - - def set_log_level(self, level): - """ - Set the logging level for the API. - - Args: - level: Logging level (e.g., logging.DEBUG, logging.INFO, logging.WARNING, self.logger.error) - """ - self.logger.setLevel(level) - self.logger.info(f"Log level set to {logging.getLevelName(level)}") - - def stop(self): - """ - Stop the API service gracefully. - """ - self.logger.info("Stopping API service...") - self.should_stop = True - - async def _run_callback_async(self, callback, *args): - """ - Run a callback asynchronously, handling both sync and async callbacks. - """ - try: - if asyncio.iscoroutinefunction(callback): - await callback(*args) - else: - callback(*args) - except Exception as e: - # Log the error but don't crash the update process - self.logger.error(f"Error in callback: {e}") - def generate_audio_message(text: str, gender: str = "male", code: str = "en-US") -> str: """ Generate a WAV file from text using Google Text-to-Speech API. @@ -412,28 +497,6 @@ class API: return file_name - def send_command(self, command: str): - """ - Send a command to the API. - - Args: - command (str): The command to send. - """ - response = self._put(command) - if response.status_code == 200: - self.logger.info(f"Command sent successfully: {command}") - else: - self.logger.error(f"Failed to send command: {response.status_code} - {response.text}") - - def run(self): - """ - Start the API service. - - This method initializes the API and starts the necessary components. - Sets up signal handlers for graceful shutdown. - """ - asyncio.run(self._run_async()) - def get_closest_units(self, coalitions: list[str], categories: list[str], position: LatLng, operate_as: str | None = None, max_number: int = 1, max_distance: float = 10000) -> list[Unit]: """ Get the closest units of a specific coalition and category to a given position. @@ -453,7 +516,7 @@ class API: # Iterate through all units and find the closest ones that match the criteria for unit in self.units.values(): - if unit.alive and unit.coalition in coalitions and unit.category.lower() in categories and (operate_as is None or unit.operate_as == operate_as or unit.coalition is not "neutral"): + if unit.alive and unit.coalition in coalitions and unit.category.lower() in categories and (operate_as is None or unit.operate_as == operate_as or unit.coalition != "neutral"): distance = position.distance_to(unit.position) if distance < closest_distance: closest_distance = distance @@ -468,39 +531,33 @@ class API: closest_units = closest_units[:max_number] return closest_units - - async def _run_async(self): - """ - Async implementation of the API service loop. - """ - # Setup signal handlers for graceful shutdown - self._setup_signal_handlers() - - # Here you can add any initialization logic if needed - self.logger.info("API started") - self.logger.info("Press Ctrl+C to stop gracefully") - - self.running = True - self.should_stop = False - - # Call the startup callback if registered - if self.on_startup_callback: - try: - await self._run_callback_async(self.on_startup_callback, self) - except Exception as e: - self.logger.error(f"Error in startup callback: {e}") - try: - while not self.should_stop: - # Update units from the last update timestamp - self.update_units(self.units_update_timestamp) - - if self.on_update_callback: - await self._run_callback_async(self.on_update_callback, self) - await asyncio.sleep(self.interval) - except KeyboardInterrupt: - self.logger.info("Keyboard interrupt received") - self.stop() - finally: - self.logger.info("API stopped") - self.running = False + def send_command(self, command: str): + """ + Send a command to the API. + + Args: + command (str): The command to send. + """ + response = self._put(command) + if response.status_code == 200: + self.logger.info(f"Command sent successfully: {command}") + else: + self.logger.error(f"Failed to send command: {response.status_code} - {response.text}") + + def stop(self): + """ + Stop the API service gracefully. + """ + self.logger.info("Stopping API service...") + self.should_stop = True + + def run(self): + """ + Start the API service. + + This method initializes the API and starts the necessary components. + Sets up signal handlers for graceful shutdown. + """ + asyncio.run(self._run_async()) + diff --git a/scripts/python/API/example_disembarked_infantry.py b/scripts/python/API/example_disembarked_infantry.py index 6ff1fd90..02c5bada 100644 --- a/scripts/python/API/example_disembarked_infantry.py +++ b/scripts/python/API/example_disembarked_infantry.py @@ -5,7 +5,7 @@ from math import pi # Setup a logger for the module import logging -logger = logging.getLogger("TestBed") +logger = logging.getLogger("example_disembarked_infantry") logger.setLevel(logging.INFO) handler = logging.StreamHandler() formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s') diff --git a/scripts/python/API/voice_control.py b/scripts/python/API/example_voice_control.py similarity index 98% rename from scripts/python/API/voice_control.py rename to scripts/python/API/example_voice_control.py index bc1b1a43..ecb5a80c 100644 --- a/scripts/python/API/voice_control.py +++ b/scripts/python/API/example_voice_control.py @@ -6,7 +6,7 @@ from radio.radio_listener import RadioListener # Setup a logger for the module import logging -logger = logging.getLogger("OlympusVoiceControl") +logger = logging.getLogger("example_voice_control") logger.setLevel(logging.INFO) handler = logging.StreamHandler() formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s') diff --git a/scripts/python/API/kronos/kronos.py b/scripts/python/API/kronos/kronos.py new file mode 100644 index 00000000..b1c2193d --- /dev/null +++ b/scripts/python/API/kronos/kronos.py @@ -0,0 +1,16 @@ +# Setup a logger for the module +import logging +logger = logging.getLogger("Kronos") +logger.setLevel(logging.INFO) +handler = logging.StreamHandler() +formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) +logger.addHandler(handler) + +class Kronos(): + def __init__(self, api): + self.api = api + + def on_startup(self): + logger.info("Kronos API started") + \ No newline at end of file diff --git a/scripts/python/API/main.py b/scripts/python/API/main.py new file mode 100644 index 00000000..5804c9f6 --- /dev/null +++ b/scripts/python/API/main.py @@ -0,0 +1,20 @@ +from api import API +from kronos.kronos import Kronos + +############################################################################################## +# Main entry point for the script. It registers the callbacks and starts the API. +############################################################################################## +if __name__ == "__main__": + # Initialize the API + api = API() + + # Initialize Kronos with the API + kronos = Kronos(api) + + # Register the callbacks + api.register_on_startup_callback(kronos.on_startup) + + # Start the API, this will run forever until stopped + api.run() + + \ No newline at end of file From 4e6701ff01a8872f888cc1ebb131b01871679531 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Fri, 8 Aug 2025 13:14:59 +0200 Subject: [PATCH 2/4] Refactor API callbacks and improve example scripts Moved register_on_update_callback in api.py for better code organization. Fixed initialization of units_to_delete and corrected simulate_fire_fight usage in example_disembarked_infantry.py. In example_voice_control.py, added cleanup of generated audio files and fixed callback parameter naming for clarity. --- scripts/python/API/api.py | 22 +++++++++---------- .../API/example_disembarked_infantry.py | 10 ++++----- scripts/python/API/example_voice_control.py | 6 ++++- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/scripts/python/API/api.py b/scripts/python/API/api.py index 60a97f3f..8f2e6adb 100644 --- a/scripts/python/API/api.py +++ b/scripts/python/API/api.py @@ -157,17 +157,7 @@ class API: except Exception as e: # Log the error but don't crash the update process self.logger.error(f"Error in callback: {e}") - - def register_on_update_callback(self, callback): - """ - Register a callback function to be called on each update. - - Args: - callback (function): The function to call on update. Can be sync or async. - The function should accept a single argument, which is the API instance. - """ - self.on_update_callback = callback - + async def _run_async(self): """ Async implementation of the API service loop. @@ -203,6 +193,16 @@ class API: finally: self.logger.info("API stopped") self.running = False + + def register_on_update_callback(self, callback): + """ + Register a callback function to be called on each update. + + Args: + callback (function): The function to call on update. Can be sync or async. + The function should accept a single argument, which is the API instance. + """ + self.on_update_callback = callback def unregister_on_update_callback(self): """ diff --git a/scripts/python/API/example_disembarked_infantry.py b/scripts/python/API/example_disembarked_infantry.py index 02c5bada..4f288bad 100644 --- a/scripts/python/API/example_disembarked_infantry.py +++ b/scripts/python/API/example_disembarked_infantry.py @@ -12,7 +12,7 @@ formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(messag handler.setFormatter(formatter) logger.addHandler(handler) -units_to_delete = None +units_to_delete = [] ############################################################################################# # This class represents a disembarked infantry unit that will engage in combat @@ -28,7 +28,7 @@ class DisembarkedInfantry(Unit): with the closest enemy unit. """ logger.info(f"Unit {self.unit_id} is now fighting.") - + # Pick a random target target = self.pick_random_target() @@ -86,7 +86,7 @@ class DisembarkedInfantry(Unit): # Simulate a firefight in the direction of the enemy firefight_destination = self.position.project_with_bearing_and_distance(30, bearing_to_enemy) - self.simulate_fire_fight(firefight_destination.lat, firefight_destination.lng, firefight_destination.alt + 1) + self.simulate_fire_fight(firefight_destination, firefight_destination.alt + 1) await asyncio.sleep(10) # Simulate some time spent in firefight self.start_fighting() # Restart the fighting process @@ -109,8 +109,8 @@ def on_api_startup(api: API): if unit.alive and not unit.human and unit.coalition == "blue": units_to_delete.append(unit) try: - unit.delete_unit(False, "", True) unit.register_on_property_change_callback("alive", on_unit_alive_change) + unit.delete_unit(False, "", True) logger.info(f"Deleted unit: {unit}") except Exception as e: @@ -175,7 +175,7 @@ def on_api_update(api: API): new_unit.__class__ = DisembarkedInfantry new_unit.start_fighting() - api.spawn_ground_units([spawn_table], unit.coalition, "", True, 0, lambda new_group_ID: execution_callback(new_group_ID)) + api.spawn_ground_units([spawn_table], unit.coalition, "", True, 0, execution_callback) logger.info(f"Spawned new unit succesfully at {spawn_position} with heading {unit.heading}") break diff --git a/scripts/python/API/example_voice_control.py b/scripts/python/API/example_voice_control.py index ecb5a80c..0a073282 100644 --- a/scripts/python/API/example_voice_control.py +++ b/scripts/python/API/example_voice_control.py @@ -1,5 +1,6 @@ from math import pi +import os from api import API, UnitSpawnTable from radio.radio_listener import RadioListener @@ -70,12 +71,15 @@ def on_message_received(recognized_text: str, unit_id: str, api: API, listener: message_filename = api.generate_audio_message("I did not understand") listener.transmit_on_frequency(message_filename, listener.frequency, listener.modulation, listener.encryption) + # Delete the message file after processing + os.remove(message_filename) + if __name__ == "__main__": api = API() logger.info("API initialized") listener = api.create_radio_listener() listener.start(frequency=251.000e6, modulation=0, encryption=0) - listener.register_message_callback(lambda wav_filename, unit_id, api=api, listener=listener: on_message_received(wav_filename, unit_id, api, listener)) + listener.register_message_callback(lambda recognized_text, unit_id, api=api, listener=listener: on_message_received(recognized_text, unit_id, api, listener)) api.run() \ No newline at end of file From 73a7ea74f314a1d7967384856b4e35e57641f7e1 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Tue, 9 Sep 2025 18:24:53 +0200 Subject: [PATCH 3/4] feat: Added threshold to unit movement --- backend/core/src/scheduler.cpp | 6 ++++++ backend/core/src/unit.cpp | 3 ++- backend/utils/include/utils.h | 1 + backend/utils/src/utils.cpp | 4 ++-- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/backend/core/src/scheduler.cpp b/backend/core/src/scheduler.cpp index 2bdb69cc..4013ceb5 100644 --- a/backend/core/src/scheduler.cpp +++ b/backend/core/src/scheduler.cpp @@ -168,6 +168,12 @@ void Scheduler::handleRequest(string key, json::value value, string username, js string WP = to_string(i); double lat = path[i][L"lat"].as_double(); double lng = path[i][L"lng"].as_double(); + if (path[i].has_number_field(L"threshold")) { + double threshold = path[i][L"threshold"].as_double(); + Coords dest; dest.lat = lat; dest.lng = lng; dest.threshold = threshold; + newPath.push_back(dest); + continue; + } Coords dest; dest.lat = lat; dest.lng = lng; newPath.push_back(dest); } diff --git a/backend/core/src/unit.cpp b/backend/core/src/unit.cpp index 952b7e56..33aa8158 100644 --- a/backend/core/src/unit.cpp +++ b/backend/core/src/unit.cpp @@ -765,6 +765,7 @@ void Unit::goToDestination(string enrouteTask) } } +// NOTE: if the current active path has a threshold set, that value will be used instead of the passed one bool Unit::isDestinationReached(double threshold) { if (activeDestination != NULL) @@ -774,7 +775,7 @@ bool Unit::isDestinationReached(double threshold) { double dist = 0; Geodesic::WGS84().Inverse(p->getPosition().lat, p->getPosition().lng, activeDestination.lat, activeDestination.lng, dist); - if (dist < threshold) + if (dist < (activeDestination.threshold == 0? threshold: activeDestination.threshold)) { log(unitName + " destination reached"); return true; diff --git a/backend/utils/include/utils.h b/backend/utils/include/utils.h index 16b16c68..70f8cf67 100644 --- a/backend/utils/include/utils.h +++ b/backend/utils/include/utils.h @@ -6,6 +6,7 @@ struct Coords { double lat = 0; double lng = 0; double alt = 0; + double threshold = 0; // used for proximity checks only, not part of the actual coordinates }; struct Offset { diff --git a/backend/utils/src/utils.cpp b/backend/utils/src/utils.cpp index a0ebfdf7..40643055 100644 --- a/backend/utils/src/utils.cpp +++ b/backend/utils/src/utils.cpp @@ -64,9 +64,9 @@ std::string random_string(size_t length) return str; } -bool operator== (const Coords& a, const Coords& b) { return a.lat == b.lat && a.lng == b.lng && a.alt == b.alt; } +bool operator== (const Coords& a, const Coords& b) { return a.lat == b.lat && a.lng == b.lng && a.alt == b.alt && a.threshold == b.threshold; } bool operator!= (const Coords& a, const Coords& b) { return !(a == b); } -bool operator== (const Coords& a, const double& b) { return a.lat == b && a.lng == b && a.alt == b; } +bool operator== (const Coords& a, const double& b) { return a.lat == b && a.lng == b && a.alt == b && a.threshold == b } bool operator!= (const Coords& a, const double& b) { return !(a == b); } bool operator== (const Offset& a, const Offset& b) { return a.x == b.x && a.y == b.y && a.z == b.z; } From 3eef91fb24ca0553b5fae3260b0662eb784a9503 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Thu, 11 Sep 2025 21:47:11 +0200 Subject: [PATCH 4/4] Add cargo weight and draw argument support Introduces cargo weight and draw argument properties to units across backend, frontend, and Python API. Adds related commands, data extraction, and registration logic, enabling setting and reading of cargo weight and custom draw arguments for units. Includes new API examples and updates to interfaces, data types, and Lua backend for full feature integration. --- backend/core/include/airunit.h | 2 + backend/core/include/commands.h | 40 +++++++++++++++++ backend/core/include/datatypes.h | 8 ++++ backend/core/include/scheduler.h | 1 + backend/core/include/unit.h | 8 +++- backend/core/src/airunit.cpp | 10 +++++ backend/core/src/commands.cpp | 23 ++++++++++ backend/core/src/datatypes.cpp | 11 +++-- backend/core/src/scheduler.cpp | 25 +++++++++++ backend/core/src/unit.cpp | 34 +++++++++++++++ backend/utils/src/utils.cpp | 2 +- frontend/react/.vscode/tasks.json | 6 --- frontend/react/src/constants/constants.ts | 2 + frontend/react/src/interfaces.ts | 7 +++ frontend/react/src/map/latlng.ts | 18 ++++++++ frontend/react/src/server/dataextractor.ts | 23 +++++++++- frontend/react/src/unit/unit.ts | 20 ++++++++- scripts/lua/backend/OlympusCommand.lua | 43 +++++++++++++++++++ scripts/python/API/.vscode/launch.json | 24 +++++++++++ scripts/python/API/data/data_extractor.py | 15 ++++++- scripts/python/API/data/data_indexes.py | 2 + scripts/python/API/data/data_types.py | 13 ++++-- scripts/python/API/example_draw_argument.py | 31 +++++++++++++ .../{main.py => example_precise_movement.py} | 16 ++++--- .../python/API/example_set_cargo_weight.py | 29 +++++++++++++ scripts/python/API/kronos/kronos.py | 16 ------- scripts/python/API/olympus.json | 2 +- scripts/python/API/unit/temp_replace.py | 18 -------- scripts/python/API/unit/unit.py | 24 +++++++++-- 29 files changed, 409 insertions(+), 64 deletions(-) create mode 100644 frontend/react/src/map/latlng.ts create mode 100644 scripts/python/API/example_draw_argument.py rename scripts/python/API/{main.py => example_precise_movement.py} (51%) create mode 100644 scripts/python/API/example_set_cargo_weight.py delete mode 100644 scripts/python/API/kronos/kronos.py delete mode 100644 scripts/python/API/unit/temp_replace.py diff --git a/backend/core/include/airunit.h b/backend/core/include/airunit.h index 66610466..ce559b68 100644 --- a/backend/core/include/airunit.h +++ b/backend/core/include/airunit.h @@ -22,6 +22,8 @@ public: virtual void setRacetrackLength(double newValue); virtual void setRacetrackAnchor(Coords newValue); virtual void setRacetrackBearing(double newValue); + + virtual void setCargoWeight(double newValue); protected: virtual void AIloop(); diff --git a/backend/core/include/commands.h b/backend/core/include/commands.h index 5119a9eb..b4fa3e68 100644 --- a/backend/core/include/commands.h +++ b/backend/core/include/commands.h @@ -538,4 +538,44 @@ public: private: const unsigned int spotID; const Coords destination; +}; + +/* Set cargo weight */ +class SetCargoWeight : public Command +{ + public: + SetCargoWeight(unsigned int ID, double weight, function callback = []() {}) : + Command(callback), + ID(ID), + weight(weight) + { + priority = CommandPriority::LOW; + }; + virtual string getString(); + virtual unsigned int getLoad() { return 5; } + +private: + const unsigned int ID; + const double weight; +}; + +/* Register draw argument */ +class RegisterDrawArgument : public Command +{ + public: + RegisterDrawArgument(unsigned int ID, unsigned int argument, bool active, function callback = []() {}) : + Command(callback), + ID(ID), + argument(argument), + active(active) + { + priority = CommandPriority::LOW; + }; + virtual string getString(); + virtual unsigned int getLoad() { return 5; } + +private: + const unsigned int ID; + const unsigned int argument; + const bool active; }; \ No newline at end of file diff --git a/backend/core/include/datatypes.h b/backend/core/include/datatypes.h index d4a9a3b0..fb52e979 100644 --- a/backend/core/include/datatypes.h +++ b/backend/core/include/datatypes.h @@ -70,6 +70,8 @@ namespace DataIndex { aimMethodRange, acquisitionRange, airborne, + cargoWeight, + drawArguments, lastIndex, endOfData = 255 }; @@ -159,6 +161,11 @@ namespace DataTypes { unsigned int ID = 0; unsigned char detectionMethod = 0; }; + + struct DrawArgument { + unsigned int argument = 0; + double value = 0.0; + }; } #pragma pack(pop) @@ -167,6 +174,7 @@ bool operator==(const DataTypes::Radio& lhs, const DataTypes::Radio& rhs); bool operator==(const DataTypes::GeneralSettings& lhs, const DataTypes::GeneralSettings& rhs); bool operator==(const DataTypes::Ammo& lhs, const DataTypes::Ammo& rhs); bool operator==(const DataTypes::Contact& lhs, const DataTypes::Contact& rhs); +bool operator==(const DataTypes::DrawArgument& lhs, const DataTypes::DrawArgument& rhs); struct SpawnOptions { string unitType; diff --git a/backend/core/include/scheduler.h b/backend/core/include/scheduler.h index 304a9da1..c64045ec 100644 --- a/backend/core/include/scheduler.h +++ b/backend/core/include/scheduler.h @@ -19,6 +19,7 @@ public: return true; } } + return false; } void setFrameRate(double newFrameRate) { frameRate = newFrameRate; } diff --git a/backend/core/include/unit.h b/backend/core/include/unit.h index 0ff980ba..fad95eee 100644 --- a/backend/core/include/unit.h +++ b/backend/core/include/unit.h @@ -130,9 +130,11 @@ public: virtual void setAcquisitionRange(double newValue) { updateValue(acquisitionRange, newValue, DataIndex::acquisitionRange); } virtual void setRadarState(bool newValue) { updateValue(radarState, newValue, DataIndex::radarState); } virtual void setAirborne(bool newValue) { updateValue(airborne, newValue, DataIndex::airborne); } + virtual void setCargoWeight(double newValue) { updateValue(cargoWeight, newValue, DataIndex::cargoWeight); } + virtual void setDrawArguments(vector newValue); /********** Getters **********/ - virtual string getCategory() { return category; }; + virtual string getCategory() { return category; } virtual bool getAlive() { return alive; } virtual unsigned char getAlarmState() { return alarmState; } virtual bool getHuman() { return human; } @@ -197,6 +199,8 @@ public: virtual double getAcquisitionRange() { return acquisitionRange; } virtual bool getRadarState() { return radarState; } virtual bool getAirborne() { return airborne; } + virtual double getCargoWeight() { return cargoWeight; } + virtual vector getDrawArguments() { return drawArguments; } protected: unsigned int ID; @@ -267,6 +271,8 @@ protected: double aimMethodRange = 0; double acquisitionRange = 0; bool airborne = false; + double cargoWeight = 0; + vector drawArguments; /********** Other **********/ unsigned int taskCheckCounter = 0; diff --git a/backend/core/src/airunit.cpp b/backend/core/src/airunit.cpp index baa33bb0..d41941e4 100644 --- a/backend/core/src/airunit.cpp +++ b/backend/core/src/airunit.cpp @@ -428,4 +428,14 @@ void AirUnit::setRacetrackBearing(double newRacetrackBearing) { triggerUpdate(DataIndex::racetrackBearing); } +} + +void AirUnit::setCargoWeight(double newCargoWeight) { + if (cargoWeight != newCargoWeight) { + cargoWeight = newCargoWeight; + triggerUpdate(DataIndex::cargoWeight); + + Command* command = dynamic_cast(new SetCargoWeight(this->ID, cargoWeight)); + scheduler->appendCommand(command); + } } \ No newline at end of file diff --git a/backend/core/src/commands.cpp b/backend/core/src/commands.cpp index b0b0c93d..75f1cf65 100644 --- a/backend/core/src/commands.cpp +++ b/backend/core/src/commands.cpp @@ -318,4 +318,27 @@ string DeleteSpot::getString() commandSS << "Olympus.deleteSpot, " << spotID; return commandSS.str(); +} + +/* SetCargoWeight command */ +string SetCargoWeight::getString() +{ + std::ostringstream commandSS; + commandSS.precision(10); + commandSS << "Olympus.setCargoWeight, " + << ID << ", " + << weight; + return commandSS.str(); +} + +/* RegisterDrawArgument command */ +string RegisterDrawArgument::getString() +{ + std::ostringstream commandSS; + commandSS.precision(10); + commandSS << "Olympus.registerDrawArgument, " + << ID << ", " + << argument << ", " + << active; + return commandSS.str(); } \ No newline at end of file diff --git a/backend/core/src/datatypes.cpp b/backend/core/src/datatypes.cpp index eb0c52cd..9ea68425 100644 --- a/backend/core/src/datatypes.cpp +++ b/backend/core/src/datatypes.cpp @@ -12,19 +12,24 @@ bool operator==(const DataTypes::Radio& lhs, const DataTypes::Radio& rhs) bool operator==(const DataTypes::GeneralSettings& lhs, const DataTypes::GeneralSettings& rhs) { - return lhs.prohibitAA == rhs.prohibitAA && lhs.prohibitAfterburner == rhs.prohibitAfterburner && lhs.prohibitAG == rhs.prohibitAG && + return lhs.prohibitAA == rhs.prohibitAA && lhs.prohibitAfterburner == rhs.prohibitAfterburner && lhs.prohibitAG == rhs.prohibitAG && lhs.prohibitAirWpn == rhs.prohibitAirWpn && lhs.prohibitJettison == rhs.prohibitJettison; } bool operator==(const DataTypes::Ammo& lhs, const DataTypes::Ammo& rhs) { - return lhs.category == rhs.category && lhs.guidance == rhs.guidance && lhs.missileCategory == rhs.missileCategory && + return lhs.category == rhs.category && lhs.guidance == rhs.guidance && lhs.missileCategory == rhs.missileCategory && lhs.quantity == rhs.quantity && strcmp(lhs.name, rhs.name) == 0; } +bool operator==(const DataTypes::DrawArgument& lhs, const DataTypes::DrawArgument& rhs) +{ + return lhs.argument == rhs.argument && lhs.value == rhs.value; +} + bool operator==(const DataTypes::Contact& lhs, const DataTypes::Contact& rhs) { - return lhs.detectionMethod == rhs.detectionMethod && lhs.ID == rhs.ID; + return lhs.detectionMethod == rhs.detectionMethod && lhs.ID == rhs.ID; } diff --git a/backend/core/src/scheduler.cpp b/backend/core/src/scheduler.cpp index 4013ceb5..8d17619d 100644 --- a/backend/core/src/scheduler.cpp +++ b/backend/core/src/scheduler.cpp @@ -827,6 +827,31 @@ void Scheduler::handleRequest(string key, json::value value, string username, js unitsManager->loadDatabases(); } /************************/ + else if (key.compare("setCargoWeight") == 0) + { + unsigned int ID = value[L"ID"].as_integer(); + Unit* unit = unitsManager->getUnit(ID); + if (unit != nullptr) { + double weight = value[L"weight"].as_double(); + unit->setCargoWeight(weight); + log(username + " set weight to unit " + unit->getUnitName() + "(" + unit->getName() + "), " + to_string(weight), true); + } + } + /************************/ + else if (key.compare("registerDrawArgument") == 0) + { + unsigned int ID = value[L"ID"].as_integer(); + Unit* unit = unitsManager->getUnit(ID); + if (unit != nullptr) { + int argument = value[L"argument"].as_integer(); + bool active = value[L"active"].as_bool(); + + command = dynamic_cast(new RegisterDrawArgument(ID, argument, active)); + + log(username + " registered draw argument " + to_string(argument) + " for unit " + unit->getUnitName() + "(" + unit->getName() + "), value:" + to_string(active), true); + } + } + /************************/ else { log("Unknown command: " + key); diff --git a/backend/core/src/unit.cpp b/backend/core/src/unit.cpp index 33aa8158..8392f542 100644 --- a/backend/core/src/unit.cpp +++ b/backend/core/src/unit.cpp @@ -128,6 +128,20 @@ void Unit::update(json::value json, double dt) setAmmo(ammo); } + if (json.has_object_field(L"drawArguments")) { + vector drawArguments; + for (auto const& el : json[L"drawArguments"].as_object()) { + DataTypes::DrawArgument drawArgumentItem; + auto drawArgumentJson = el.second; + if (drawArgumentJson.has_number_field(L"argument")) + drawArgumentItem.argument = drawArgumentJson[L"argument"].as_number().to_uint32(); + if (drawArgumentJson.has_number_field(L"value")) + drawArgumentItem.value = drawArgumentJson[L"value"].as_number().to_double(); + drawArguments.push_back(drawArgumentItem); + } + setDrawArguments(drawArguments); + } + if (json.has_object_field(L"contacts")) { vector contacts; for (auto const& el : json[L"contacts"].as_object()) { @@ -328,6 +342,8 @@ void Unit::getData(stringstream& ss, unsigned long long time) case DataIndex::aimMethodRange: appendNumeric(ss, datumIndex, aimMethodRange); break; case DataIndex::acquisitionRange: appendNumeric(ss, datumIndex, acquisitionRange); break; case DataIndex::airborne: appendNumeric(ss, datumIndex, airborne); break; + case DataIndex::cargoWeight: appendNumeric(ss, datumIndex, cargoWeight); break; + case DataIndex::drawArguments: appendVector(ss, datumIndex, drawArguments); break; } } } @@ -699,6 +715,24 @@ void Unit::setGeneralSettings(DataTypes::GeneralSettings newGeneralSettings, boo } } +void Unit::setDrawArguments(vector newDrawArguments) +{ + if (drawArguments.size() == newDrawArguments.size()) { + bool equal = true; + for (int i = 0; i < drawArguments.size(); i++) { + if (drawArguments.at(i) != newDrawArguments.at(i)) + { + equal = false; + break; + } + } + if (equal) + return; + } + drawArguments = newDrawArguments; + triggerUpdate(DataIndex::drawArguments); +} + void Unit::setDesiredSpeed(double newDesiredSpeed) { if (desiredSpeed != newDesiredSpeed) { diff --git a/backend/utils/src/utils.cpp b/backend/utils/src/utils.cpp index 40643055..d2097a39 100644 --- a/backend/utils/src/utils.cpp +++ b/backend/utils/src/utils.cpp @@ -66,7 +66,7 @@ std::string random_string(size_t length) bool operator== (const Coords& a, const Coords& b) { return a.lat == b.lat && a.lng == b.lng && a.alt == b.alt && a.threshold == b.threshold; } bool operator!= (const Coords& a, const Coords& b) { return !(a == b); } -bool operator== (const Coords& a, const double& b) { return a.lat == b && a.lng == b && a.alt == b && a.threshold == b } +bool operator== (const Coords& a, const double& b) { return a.lat == b && a.lng == b && a.alt == b && a.threshold == b; } bool operator!= (const Coords& a, const double& b) { return !(a == b); } bool operator== (const Offset& a, const Offset& b) { return a.x == b.x && a.y == b.y && a.z == b.z; } diff --git a/frontend/react/.vscode/tasks.json b/frontend/react/.vscode/tasks.json index 586890ef..fb796833 100644 --- a/frontend/react/.vscode/tasks.json +++ b/frontend/react/.vscode/tasks.json @@ -1,12 +1,6 @@ { "version": "2.0.0", "tasks": [ - { - "label": "check-setup", - "type": "shell", - "command": "cd .. ; ./check_setup.bat", - "isBackground": false - }, { "type": "npm", "script": "dev", diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts index b2c39e16..e95c7267 100644 --- a/frontend/react/src/constants/constants.ts +++ b/frontend/react/src/constants/constants.ts @@ -547,6 +547,8 @@ export enum DataIndexes { aimMethodRange, acquisitionRange, airborne, + cargoWeight, + drawingArguments, endOfData = 255, } diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts index 281ea04d..f32c5bb8 100644 --- a/frontend/react/src/interfaces.ts +++ b/frontend/react/src/interfaces.ts @@ -219,6 +219,11 @@ export interface Offset { z: number; } +export interface DrawingArgument { + argument: number; + value: number; +} + export interface UnitData { category: string; markerCategory: string; @@ -286,6 +291,8 @@ export interface UnitData { aimMethodRange: number; acquisitionRange: number; airborne: boolean; + cargoWeight: number; + drawingArguments: DrawingArgument[]; } export interface LoadoutItemBlueprint { diff --git a/frontend/react/src/map/latlng.ts b/frontend/react/src/map/latlng.ts new file mode 100644 index 00000000..87c5d949 --- /dev/null +++ b/frontend/react/src/map/latlng.ts @@ -0,0 +1,18 @@ +import * as L from "leaflet"; + +export class LatLng extends L.LatLng { + threshold: number; + + constructor(lat: number, lng: number, alt: number, threshold: number) { + super(lat, lng, alt); + this.threshold = threshold; + } + + setThreshold(threshold: number) { + this.threshold = threshold; + } + + getThreshold() { + return this.threshold; + } +} diff --git a/frontend/react/src/server/dataextractor.ts b/frontend/react/src/server/dataextractor.ts index 5eec77cb..9b2e87d6 100644 --- a/frontend/react/src/server/dataextractor.ts +++ b/frontend/react/src/server/dataextractor.ts @@ -1,5 +1,5 @@ import { LatLng } from "leaflet"; -import { Ammo, Contact, GeneralSettings, Offset, Radio, TACAN } from "../interfaces"; +import { Ammo, Contact, DrawingArgument, GeneralSettings, Offset, Radio, TACAN } from "../interfaces"; export class DataExtractor { #seekPosition = 0; @@ -58,7 +58,9 @@ export class DataExtractor { } extractLatLng() { - return new LatLng(this.extractFloat64(), this.extractFloat64(), this.extractFloat64()); + let latlng = new LatLng(this.extractFloat64(), this.extractFloat64(), this.extractFloat64()); + let threshold = this.extractFloat64(); + return latlng; } extractFromBitmask(bitmask: number, position: number) { @@ -104,6 +106,14 @@ export class DataExtractor { return value; } + extractDrawingArgument() { + const value: DrawingArgument = { + argument: this.extractUInt32(), + value: this.extractFloat64(), + }; + return value; + } + extractGeneralSettings() { const value: GeneralSettings = { prohibitJettison: this.extractBool(), @@ -159,4 +169,13 @@ export class DataExtractor { }; return value; } + + extractDrawingArguments() { + const value: DrawingArgument[] = []; + const size = this.extractUInt16(); + for (let idx = 0; idx < size; idx++) { + value.push(this.extractDrawingArgument()); + } + return value; + } } diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index ed07cd62..d8074cad 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -1,4 +1,4 @@ -import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map, Point, LeafletMouseEvent, DomEvent, DomUtil, Circle } from "leaflet"; +import { LatLng, Polyline, DivIcon, CircleMarker, Map, Point, DomEvent } from "leaflet"; import { getApp } from "../olympusapp"; import { enumToCoalition, @@ -54,7 +54,7 @@ import { } from "../constants/constants"; import { DataExtractor } from "../server/dataextractor"; import { Weapon } from "../weapon/weapon"; -import { AlarmState, Ammo, Contact, GeneralSettings, LoadoutBlueprint, ObjectIconOptions, Offset, Radio, TACAN, UnitBlueprint, UnitData } from "../interfaces"; +import { AlarmState, Ammo, Contact, DrawingArgument, GeneralSettings, LoadoutBlueprint, ObjectIconOptions, Offset, Radio, TACAN, UnitBlueprint, UnitData } from "../interfaces"; import { RangeCircle } from "../map/rangecircle"; import { Group } from "./group"; import { ContextActionSet } from "./contextactionset"; @@ -159,6 +159,8 @@ export abstract class Unit extends CustomMarker { #racetrackAnchor: LatLng = new LatLng(0, 0); #racetrackBearing: number = 0; #airborne: boolean = false; + #cargoWeight: number = 0; + #drawingArguments: DrawingArgument[] = []; /* Other members used to draw the unit, mostly ancillary stuff like targets, ranges and so on */ #blueprint: UnitBlueprint | null = null; @@ -406,6 +408,12 @@ export abstract class Unit extends CustomMarker { getAirborne() { return this.#airborne; } + getCargoWeight() { + return this.#cargoWeight; + } + getDrawingArguments() { + return this.#drawingArguments; + } static getConstructor(type: string) { if (type === "GroundUnit") return GroundUnit; @@ -797,6 +805,12 @@ export abstract class Unit extends CustomMarker { case DataIndexes.airborne: this.#airborne = dataExtractor.extractBool(); break; + case DataIndexes.cargoWeight: + this.#cargoWeight = dataExtractor.extractFloat64(); + break; + case DataIndexes.drawingArguments: + this.#drawingArguments = dataExtractor.extractDrawingArguments(); + break; default: break; } @@ -920,6 +934,8 @@ export abstract class Unit extends CustomMarker { aimMethodRange: this.#aimMethodRange, acquisitionRange: this.#acquisitionRange, airborne: this.#airborne, + cargoWeight: this.#cargoWeight, + drawingArguments: this.#drawingArguments, }; } diff --git a/scripts/lua/backend/OlympusCommand.lua b/scripts/lua/backend/OlympusCommand.lua index 4d3e8146..c098635c 100644 --- a/scripts/lua/backend/OlympusCommand.lua +++ b/scripts/lua/backend/OlympusCommand.lua @@ -23,6 +23,7 @@ Olympus.unitIndex = 0 -- Counter used to spread the computational load of data Olympus.unitStep = 50 -- Max number of units that get updated each cycle Olympus.units = {} -- Table holding references to all the currently existing units Olympus.unitsInitialLife = {} -- getLife0 returns 0 for ships, so we need to store the initial life of units +Olympus.drawArguments = {} -- Table that sets what drawArguments to read for each unit Olympus.weaponIndex = 0 -- Counter used to spread the computational load of data retrievial from DCS Olympus.weaponStep = 50 -- Max number of weapons that get updated each cycle @@ -1087,10 +1088,38 @@ function Olympus.setOnOff(groupName, onOff) end end +-- Get the unit description function getUnitDescription(unit) return unit:getDescr() end +-- Set the unit cargo weight +function Olympus.setCargoWeight(ID, weight) + Olympus.debug("Olympus.setCargoWeight " .. ID .. " " .. tostring(weight), 2) + + local unit = Olympus.getUnitByID(ID) + if unit ~= nil and unit:isExist() then + trigger.action.setUnitInternalCargo(unit:getName(), weight) + end +end + +-- Register a drawArgument to be read for a unit +function Olympus.registerDrawArgument(ID, argument, active) + Olympus.debug("Olympus.registerDrawArgument " .. ID .. " " .. tostring(argument) .. " " .. tostring(active), 2) + + -- Create the table if it does not exist + if Olympus.drawArguments[ID] == nil then + Olympus.drawArguments[ID] = {} + end + + -- Set the draw argument to true or false + if active then + Olympus.drawArguments[ID][argument] = true + else + Olympus.drawArguments[ID][argument] = false + end +end + -- This function gets the navpoints from the DCS mission function Olympus.getNavPoints() local function extract_tag(str) @@ -1293,6 +1322,20 @@ function Olympus.setUnitsData(arg, time) table["radarState"] = false end end ]] + + -- Read the draw arguments + local drawArguments = {} + if Olympus.drawArguments[ID] ~= nil then + for argument, active in pairs(Olympus.drawArguments[ID]) do + if active then + drawArguments[#drawArguments + 1] = { + argument = argument, + value = unit:getDrawArgumentValue(argument) + } + end + end + end + table["drawArguments"] = drawArguments local group = unit:getGroup() if group ~= nil then diff --git a/scripts/python/API/.vscode/launch.json b/scripts/python/API/.vscode/launch.json index 59e84c31..9870d49a 100644 --- a/scripts/python/API/.vscode/launch.json +++ b/scripts/python/API/.vscode/launch.json @@ -27,6 +27,30 @@ "program": "example_disembarked_infantry.py", "console": "integratedTerminal", "justMyCode": false, + }, + { + "name": "Example set cargo weight", + "type": "debugpy", + "request": "launch", + "program": "example_set_cargo_weight.py", + "console": "integratedTerminal", + "justMyCode": false, + }, + { + "name": "Example draw argument", + "type": "debugpy", + "request": "launch", + "program": "example_draw_argument.py", + "console": "integratedTerminal", + "justMyCode": false, + }, + { + "name": "Example precise movement", + "type": "debugpy", + "request": "launch", + "program": "example_precise_movement.py", + "console": "integratedTerminal", + "justMyCode": false, } ] } \ No newline at end of file diff --git a/scripts/python/API/data/data_extractor.py b/scripts/python/API/data/data_extractor.py index de1608dd..bdcfebaf 100644 --- a/scripts/python/API/data/data_extractor.py +++ b/scripts/python/API/data/data_extractor.py @@ -1,6 +1,6 @@ import struct from typing import List -from data.data_types import LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset +from data.data_types import DrawArgument, LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset class DataExtractor: def __init__(self, buffer: bytes): @@ -48,6 +48,7 @@ class DataExtractor: lat = self.extract_float64() lng = self.extract_float64() alt = self.extract_float64() + threshold = self.extract_float64() return LatLng(lat, lng, alt) def extract_from_bitmask(self, bitmask: int, position: int) -> bool: @@ -136,4 +137,14 @@ class DataExtractor: x=self.extract_float64(), y=self.extract_float64(), z=self.extract_float64() - ) \ No newline at end of file + ) + + def extract_draw_arguments(self) -> List[DrawArgument]: + value = [] + size = self.extract_uint16() + for _ in range(size): + value.append(DrawArgument( + argument=self.extract_uint32(), + value=self.extract_float64() + )) + return value \ No newline at end of file diff --git a/scripts/python/API/data/data_indexes.py b/scripts/python/API/data/data_indexes.py index 38155fb2..54715d97 100644 --- a/scripts/python/API/data/data_indexes.py +++ b/scripts/python/API/data/data_indexes.py @@ -67,4 +67,6 @@ class DataIndexes(Enum): AIM_METHOD_RANGE = 63 ACQUISITION_RANGE = 64 AIRBORNE = 65 + CARGO_WEIGHT = 66 + DRAW_ARGUMENTS = 67 END_OF_DATA = 255 \ No newline at end of file diff --git a/scripts/python/API/data/data_types.py b/scripts/python/API/data/data_types.py index c4017a31..ffd73f27 100644 --- a/scripts/python/API/data/data_types.py +++ b/scripts/python/API/data/data_types.py @@ -8,13 +8,15 @@ class LatLng: lat: float lng: float alt: float - + threshold: Optional[float] = 0 # Optional threshold for proximity checks + def toJSON(self): """Convert LatLng to a JSON serializable dictionary.""" return { "lat": self.lat, "lng": self.lng, - "alt": self.alt + "alt": self.alt, + "threshold": self.threshold } def project_with_bearing_and_distance(self, d, bearing): @@ -88,4 +90,9 @@ class Contact: class Offset: x: float y: float - z: float \ No newline at end of file + z: float + +@dataclass +class DrawArgument: + argument: int + value: float \ No newline at end of file diff --git a/scripts/python/API/example_draw_argument.py b/scripts/python/API/example_draw_argument.py new file mode 100644 index 00000000..b297c099 --- /dev/null +++ b/scripts/python/API/example_draw_argument.py @@ -0,0 +1,31 @@ +from api import API + +def on_api_startup(api: API): + units = api.update_units() + for unit in units.values(): + if unit.name == "UH-1H": + # Register draw argument 43 for UH-1H + unit.register_draw_argument(43) + +def on_api_update(api: API): + units = api.get_units() + for unit in units.values(): + if unit.name == "UH-1H": + print(f"Draw Arguments for {unit.name}:") + for draw_arg in unit.draw_arguments: + print(f" Argument: {draw_arg.argument}, Value: {draw_arg.value}") + +############################################################################################## +# Main entry point for the script. It registers the callbacks and starts the API. +############################################################################################## +if __name__ == "__main__": + # Initialize the API + api = API() + + # Register the callbacks + api.register_on_update_callback(on_api_update) + api.register_on_startup_callback(on_api_startup) + + # Start the API, this will run forever until stopped + api.run() + \ No newline at end of file diff --git a/scripts/python/API/main.py b/scripts/python/API/example_precise_movement.py similarity index 51% rename from scripts/python/API/main.py rename to scripts/python/API/example_precise_movement.py index 5804c9f6..fc4bf013 100644 --- a/scripts/python/API/main.py +++ b/scripts/python/API/example_precise_movement.py @@ -1,5 +1,13 @@ from api import API -from kronos.kronos import Kronos + +def on_api_startup(api: API): + units = api.update_units() + for unit in units.values(): + if unit.name == "Infantry AK Ins": + current_pos = unit.position + next_pos = current_pos.project_with_bearing_and_distance(20, 0) # Move 20 meters north + next_pos.threshold = 2 # Set threshold to 1 meter, very precise + unit.set_path([next_pos]) ############################################################################################## # Main entry point for the script. It registers the callbacks and starts the API. @@ -8,13 +16,9 @@ if __name__ == "__main__": # Initialize the API api = API() - # Initialize Kronos with the API - kronos = Kronos(api) - # Register the callbacks - api.register_on_startup_callback(kronos.on_startup) + api.register_on_startup_callback(on_api_startup) # Start the API, this will run forever until stopped api.run() - \ No newline at end of file diff --git a/scripts/python/API/example_set_cargo_weight.py b/scripts/python/API/example_set_cargo_weight.py new file mode 100644 index 00000000..c62cabe9 --- /dev/null +++ b/scripts/python/API/example_set_cargo_weight.py @@ -0,0 +1,29 @@ +from api import API + +def on_api_startup(api: API): + units = api.update_units() + for unit in units.values(): + if unit.name == "UH-1H": + # Set cargo weight to 5000 kg + unit.set_cargo_weight(5000.0) + +def on_api_update(api: API): + units = api.get_units() + for unit in units.values(): + if unit.name == "UH-1H": + print(f"Cargo Weight for {unit.name}: {unit.cargo_weight} kg") + +############################################################################################## +# Main entry point for the script. It registers the callbacks and starts the API. +############################################################################################## +if __name__ == "__main__": + # Initialize the API + api = API() + + # Register the callbacks + api.register_on_update_callback(on_api_update) + api.register_on_startup_callback(on_api_startup) + + # Start the API, this will run forever until stopped + api.run() + \ No newline at end of file diff --git a/scripts/python/API/kronos/kronos.py b/scripts/python/API/kronos/kronos.py deleted file mode 100644 index b1c2193d..00000000 --- a/scripts/python/API/kronos/kronos.py +++ /dev/null @@ -1,16 +0,0 @@ -# Setup a logger for the module -import logging -logger = logging.getLogger("Kronos") -logger.setLevel(logging.INFO) -handler = logging.StreamHandler() -formatter = logging.Formatter('[%(asctime)s] %(name)s - %(levelname)s - %(message)s') -handler.setFormatter(formatter) -logger.addHandler(handler) - -class Kronos(): - def __init__(self, api): - self.api = api - - def on_startup(self): - logger.info("Kronos API started") - \ No newline at end of file diff --git a/scripts/python/API/olympus.json b/scripts/python/API/olympus.json index 7159c6ee..b81d60e5 100644 --- a/scripts/python/API/olympus.json +++ b/scripts/python/API/olympus.json @@ -4,7 +4,7 @@ "port": 4512 }, "authentication": { - "gameMasterPassword": "a474219e5e9503c84d59500bb1bda3d9ade81e52d9fa1c234278770892a6dd74", + "gameMasterPassword": "a00a5973aacb17e4659125fbe10f4160d096dd84b2f586d2d75669462a30106d", "blueCommanderPassword": "7d2e1ef898b21db7411f725a945b76ec8dcad340ed705eaf801bc82be6fe8a4a", "redCommanderPassword": "abc5de7abdb8ed98f6d11d22c9d17593e339fde9cf4b9e170541b4f41af937e3" }, diff --git a/scripts/python/API/unit/temp_replace.py b/scripts/python/API/unit/temp_replace.py deleted file mode 100644 index 8fe34a54..00000000 --- a/scripts/python/API/unit/temp_replace.py +++ /dev/null @@ -1,18 +0,0 @@ -import re - -# Read the file -with open('unit.py', 'r', encoding='utf-8') as f: - content = f.read() - -# Pattern to match callback invocations -pattern = r'self\.on_property_change_callbacks\[\"(\w+)\"\]\(self, self\.(\w+)\)' -replacement = r'self._trigger_callback("\1", self.\2)' - -# Replace all matches -new_content = re.sub(pattern, replacement, content) - -# Write back to file -with open('unit.py', 'w', encoding='utf-8') as f: - f.write(new_content) - -print('Updated all callback invocations') diff --git a/scripts/python/API/unit/unit.py b/scripts/python/API/unit/unit.py index f64cdfa5..dc8e1d0f 100644 --- a/scripts/python/API/unit/unit.py +++ b/scripts/python/API/unit/unit.py @@ -3,7 +3,7 @@ import asyncio from data.data_extractor import DataExtractor from data.data_indexes import DataIndexes -from data.data_types import LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset +from data.data_types import DrawArgument, LatLng, TACAN, Radio, GeneralSettings, Ammo, Contact, Offset from data.roes import ROES from data.states import states from utils.utils import enum_to_coalition @@ -81,6 +81,8 @@ class Unit: self.targeting_range = 0.0 self.aim_method_range = 0.0 self.acquisition_range = 0.0 + self.cargo_weight = 0.0 + self.draw_arguments: List[DrawArgument] = [] self.previous_total_ammo = 0 self.total_ammo = 0 @@ -654,6 +656,20 @@ class Unit: # Trigger callbacks for property change if "airborne" in self.on_property_change_callbacks: self._trigger_callback("airborne", self.airborne) + elif datum_index == DataIndexes.CARGO_WEIGHT.value: + cargo_weight = data_extractor.extract_float64() + if cargo_weight != self.cargo_weight: + self.cargo_weight = cargo_weight + # Trigger callbacks for property change + if "cargo_weight" in self.on_property_change_callbacks: + self._trigger_callback("cargo_weight", self.cargo_weight) + elif datum_index == DataIndexes.DRAW_ARGUMENTS.value: + draw_arguments = data_extractor.extract_draw_arguments() + if draw_arguments != self.draw_arguments: + self.draw_arguments = draw_arguments + # Trigger callbacks for property change + if "draw_arguments" in self.on_property_change_callbacks: + self._trigger_callback("draw_arguments", self.draw_arguments) # --- API functions requiring ID --- def set_path(self, path: List[LatLng]): @@ -758,6 +774,8 @@ class Unit: def set_engagement_properties(self, barrel_height, muzzle_velocity, aim_time, shots_to_fire, shots_base_interval, shots_base_scatter, engagement_range, targeting_range, aim_method_range, acquisition_range): return self.api.send_command({"setEngagementProperties": {"ID": self.ID, "barrelHeight": barrel_height, "muzzleVelocity": muzzle_velocity, "aimTime": aim_time, "shotsToFire": shots_to_fire, "shotsBaseInterval": shots_base_interval, "shotsBaseScatter": shots_base_scatter, "engagementRange": engagement_range, "targetingRange": targeting_range, "aimMethodRange": aim_method_range, "acquisitionRange": acquisition_range}}) - + def set_cargo_weight(self, cargo_weight: float): + return self.api.send_command({"setCargoWeight": {"ID": self.ID, "weight": cargo_weight}}) - \ No newline at end of file + def register_draw_argument(self, argument: int, active: bool = True): + return self.api.send_command({"registerDrawArgument": {"ID": self.ID, "argument": argument, "active": active}}) \ No newline at end of file